use OpenILS::Application::AppUtils;
my $U = "OpenILS::Application::AppUtils";
-# used in build_hold_sort_clause()
+# Used in build_hold_sort_clause(). See the hash %order_by_sprintf_args in
+# that sub to confirm what gets used to replace the formatters, and see
+# nearest_hold() for the main body of the SQL query that these go into.
my %HOLD_SORT_ORDER_BY = (
pprox => 'p.prox',
- hprox => 'actor.org_unit_proximity(%d, h.request_lib)', # $cp->circ_lib
+ hprox => 'actor.org_unit_proximity(%d, h.pickup_lib)', # $cp->call_number->owning_lib
aprox => 'COALESCE(hm.proximity, p.prox)',
approx => 'action.hold_copy_calculated_proximity(h.id, %d, %d)', # $cp,$here
priority => 'pgt.hold_priority',
rtime => 'h.request_time',
htime => q!
CASE WHEN
+ last_event_on_copy.place <> %d AND
copy_has_not_been_home.result
- THEN actor.org_unit_proximity(%d, h.request_lib)
+ THEN actor.org_unit_proximity(%d, h.pickup_lib)
ELSE 999
END
- !,
+ !, # $cp->call_number->owning_lib x 2
shtime => q!
CASE WHEN
+ last_event_on_copy.place <> %d AND
copy_has_not_been_home_even_to_idle.result
- THEN actor.org_unit_proximity(%d, h.request_lib)
+ THEN actor.org_unit_proximity(%d, h.pickup_lib)
ELSE 999
END
- !,
+ !, # $cp->call_number->owning_lib x 2
);
my ($columns, $cp, $here) = @_;
my %order_by_sprintf_args = (
- hprox => [$cp->circ_lib],
+ hprox => [$cp->call_number->owning_lib],
approx => [$cp->id, $here],
- htime => [$cp->circ_lib],
- shtime => [$cp->circ_lib]
+ htime => [$cp->call_number->owning_lib, $cp->call_number->owning_lib],
+ shtime => [$cp->call_number->owning_lib, $cp->call_number->owning_lib]
);
my @clauses;
# more order-by clauses after that.
}
- my ($ctes, $joins);
+ my ($ctes, $joins) = ("", "");
if ($ctes_needed >= 1) {
- # For our first auxiliary query, the question we seek to answer is, "has
- # our copy been circulating away from home too long?" Two parts to
- # answer this question.
+ # Each CTE serves the next. The first is one version or another
+ # of last_event_on_copy, which is described in holds-go-home.txt
+ # TechRef, but it essentially returns place and time of the most
+ # recent transit or circ to do with a copy, and failing that it
+ # returns a synthetic event that means "here" and "now".
+
+ if ($ctes_needed == 2) {
+ $ctes .= sprintf(q!
+, last_event_on_copy AS ( -- combined circ and transit version
+ SELECT *
+ FROM (
+ ( SELECT
+ TRUE AS concrete,
+ dest AS place,
+ COALESCE(dest_recv_time, source_send_time) AS moment
+ FROM action.transit_copy
+ WHERE target_copy = %d
+ ORDER BY moment DESC LIMIT 1
+ ) UNION (
+ SELECT
+ TRUE AS concrete,
+ COALESCE(checkin_lib, circ_lib) AS place,
+ COALESCE(checkin_time, xact_start) AS moment
+ FROM action.circulation
+ WHERE target_copy = %d
+ ORDER BY moment DESC LIMIT 1
+ ) UNION
+ SELECT
+ FALSE AS concrete,
+ %d AS place,
+ NOW() AS moment
+ ) x ORDER BY concrete DESC, moment DESC LIMIT 1
+) !, $cp->id, $cp->id, $cp->call_number->owning_lib);
+ } else {
+ $ctes .= sprintf(q!
+, last_event_on_copy AS ( -- circ only version
+ SELECT * FROM (
+ ( SELECT
+ TRUE AS concrete,
+ COALESCE(checkin_lib, circ_lib) AS place,
+ COALESCE(checkin_time, xact_start) AS moment
+ FROM action.circulation
+ WHERE target_copy = %d
+ ORDER BY moment DESC LIMIT 1
+ ) UNION SELECT
+ FALSE AS concrete,
+ %d AS place,
+ NOW() AS moment
+ ) x ORDER BY concrete DESC, moment DESC LIMIT 1
+) !, $cp->id, $cp->call_number->owning_lib);
+ }
+
+ $joins .= q!
+ JOIN last_event_on_copy ON (true)
+ !;
+
+ # For our next auxiliary query, the question we seek to answer is,
+ # "has our copy been circulating away from home too long?"
#
- # part 1: Have their been no checkouts at the copy's circ_lib since the
+ # Have there been no checkouts at the copy's circ_lib since the
# beginning of our go-home interval?
- # part 2: Was the last transit to affect our copy before the beginning
- # of our go-home interval an outbound transit? i.e. away from circ-lib
# [We use sprintf because the outer function that's going to send one
# big query through DBI is blind to our process of dynamically building
circ.target_copy = %d AND
circ.circ_lib = %d AND
circ.xact_start >= NOW() - go_home_interval.value
- ) IS NULL AND (
- -- part 2
- SELECT atc.dest <> %d FROM action.transit_copy atc
- JOIN go_home_interval ON (true)
- WHERE
- atc.id = (
- SELECT MAX(id) FROM action.transit_copy atc_inner
- WHERE
- atc_inner.target_copy = %d AND
- atc_inner.source_send_time < NOW() - go_home_interval.value
- )
- ) AS result
-) !, $cp->id, $cp->circ_lib, $cp->circ_lib, $cp->id);
- $joins .= " JOIN copy_has_not_been_home ON (true) ";
+ ) IS NULL AS result
+) !, $cp->id, $cp->circ_lib);
+
+ $joins .= q!
+ JOIN copy_has_not_been_home ON (true)
+ !;
}
if ($ctes_needed == 2) {
- # In this auxiliary query, we ask the question, "has our copy come home
- # by any means that we can determine, even if it didn't circulate once
- # it came home, in the time defined by the go-home-interval?"
- # answer this question. Two parts to this too (besides including the
- # previous auxiliary query).
+ # By this query, we mean to determine that the copy hasn't landed at
+ # home by means of transit during the go-home interval (in addition
+ # to not having circulated from home in the same time frame).
#
- # 1: there have been no homebound transits for this copy since the
- # beginning of the go-home interval.
- # 2: there have been no checkins at home since the beginning of
- # the go-home interval for this copy
+ # There have been no homebound transits that arrived for this copy
+ # since the beginning of the go-home interval.
$ctes .= sprintf(q!
, copy_has_not_been_home_even_to_idle AS (
- SELECT
- copy_has_not_been_home.response AND (
- -- part 1
- SELECT MIN(atc.id) FROM action.transit_copy atc
- JOIN go_home_interval ON (true)
- WHERE
- atc.target_copy = %d AND
- atc.dest = %d AND
- atc.dest_recv_time >= NOW() - go_home_interval.value
- ) IS NULL AND (
- -- part 2
- SELECT MIN(circ.id) FROM action.circulation circ
- JOIN go_home_interval ON (true)
- WHERE
- circ.target_copy = %d AND
- circ.checkin_lib = %d AND
- circ.checkin_time >= NOW() - go_home_interval.value
- ) IS NULL
- AS result
-) !, $cp->id, $cp->circ_lib, $cp->id, $cp->circ_lib);
+ SELECT result AND NOT (
+ SELECT COUNT(*)::INT::BOOL
+ FROM action.transit_copy atc
+ WHERE
+ atc.target_copy = %d AND
+ (atc.dest = %d OR atc.source = %d) AND
+ atc.dest_recv_time >= NOW() - (SELECT value FROM go_home_interval)
+ ) AS result FROM copy_has_not_been_home
+) !, $cp->id, $cp->circ_lib, $cp->circ_lib);
$joins .= " JOIN copy_has_not_been_home_even_to_idle ON (true) ";
}
my $self = shift;
my $client = shift;
my $here = shift; # just the ID
- my $cp = shift; # now an object, formerly just the ID
+ my $cp = shift; # now an object with call_number fleshed,
+ # formerly just copy ID
my $limit = int(shift()) || 10;
my $age = shift() || '0 seconds';
my $fifo = shift();
--- /dev/null
+Holds Go Home
+=============
+
+Outline
+-------
+
+A copy prefers to fulfill a hold near its home when:
+
+ - The last event for a copy was NOT at home *and* ...
+ - The copy has not circulated from home within the defined period *and* ...
+ - The copy has neither departed from home by transit nor arrived at home
+ by transit within the defined period.
+
+Definitions
+-----------
+
+In the preceding section, some terms are used that want explanation.
+
+_Event_ refers to either a circulation or a transit related to a copy. An
+event has only two qualities we care about: moment and place.
+
+ - When the event comes from a circulation, _moment_ is checkin_time if
+ we have it, otherwise xact_start. When the event comes from a transit,
+ moment is dest_recv_time if we have it, otherwise source_send_time.
+
+ - When the event comes from a circulation, _place_ is checkin_lib if we
+ have it, otherwise circ_lib. When the event comes from a transit,
+ place is dest, *always*.
+
+ - When the copy in question has neither any transit history nor any
+ circulation history, we produce a synthetic event with _moment_ equal
+ to the present time, and _place_ equal to the copy's call number's
+ owning_lib (see 'home' below).
+
+_Home_ is the value of the copy's call number's owning_lib field, which
+incidentally is usually equal to the copy's circ_lib field except where
+floating is in use.
+
+_The defined period_ is the time between the present moment (_NOW()_
+to the database) and the present moment less the value of the interval
+defined in the _circ.hold_go_home_interval_ org unit setting at a scope
+of the copy's _home_. E.g., if the setting contains the string "6 months",
+the defined period is the last 6 months before the present moment, or
+anything greater than _NOW() - '6 months'::INTERVAL_.
+
+Logic
+-----
+
+............................................
+
+ -------
+| Event |
+ -------
+ |
+ |
+ v
+ ------------------------- -----------------------
+| Was last event at home? | -----Yes.-----> | Don't try to go home. |
+ ------------------------- -----------------------
+ | ^ ^
+ | No. | |
+ v | |
+ ------------------------------------------------ | |
+| Did copy circ from home during defined period? |--Yes.--/ |
+ ------------------------------------------------ |
+ | |
+ | No. |
+ | |
+ v |
+ ------------------------------------------------------ |
+| Did copy leave or arrive home during defined period? |--Yes.---/
+ ------------------------------------------------------
+ |
+ | No.
+ |
+ v
+ ----------
+| Go home. |
+ ----------
+
+............................................
+
+
+Implications in Best-Hold Selection Sort Order
+----------------------------------------------
+
+The calculations described above are all embodied in the best-hold selection
+sort order determinant _shtime_.
+
+_htime_ is a simpler version of the same, with all reference to transits
+removed, considering only circulations. This means events become thin
+circulations, the "Did a transit bring copy home..." step in the flow chart
+goes away, etc. etc.