]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
LP#1549505: schema and IDL for statistical poularity ratings
[Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / XXXX.schema.statisitcal-ratings.sql
1
2 BEGIN;
3
4 -- Create these so that the queries in the UDFs will validate
5 CREATE TEMP TABLE precalc_filter_bib_list (
6     id  BIGINT
7 ) ON COMMIT DROP;
8
9 CREATE TEMP TABLE precalc_bib_filter_bib_list (
10     id  BIGINT
11 ) ON COMMIT DROP;
12
13 CREATE TEMP TABLE precalc_src_filter_bib_list (
14     id  BIGINT
15 ) ON COMMIT DROP;
16
17 CREATE TEMP TABLE precalc_copy_filter_bib_list (
18     id  BIGINT,
19     copy  BIGINT
20 ) ON COMMIT DROP;
21
22 CREATE TEMP TABLE precalc_circ_mod_filter_bib_list (
23     id  BIGINT,
24     copy  BIGINT
25 ) ON COMMIT DROP;
26
27 CREATE TEMP TABLE precalc_location_filter_bib_list (
28     id  BIGINT,
29     copy  BIGINT
30 ) ON COMMIT DROP;
31
32 CREATE TEMP TABLE precalc_attr_filter_bib_list (
33     id  BIGINT
34 ) ON COMMIT DROP;
35
36 CREATE TEMP TABLE precalc_bibs_by_copy_list (
37     id  BIGINT
38 ) ON COMMIT DROP;
39
40 CREATE TEMP TABLE precalc_bibs_by_uri_list (
41     id  BIGINT
42 ) ON COMMIT DROP;
43
44 CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list (
45     id  BIGINT
46 ) ON COMMIT DROP;
47
48 CREATE TEMP TABLE precalc_bib_list (
49     id  BIGINT
50 ) ON COMMIT DROP;
51
52 -- rhrr needs to be a real table, so it can be fast. To that end, we use
53 -- a materialized view updated via a trigger.
54
55 DROP VIEW reporter.hold_request_record;
56
57 CREATE TABLE reporter.hold_request_record  AS
58 SELECT  id,
59         target,
60         hold_type,
61         CASE
62                 WHEN hold_type = 'T'
63                         THEN target
64                 WHEN hold_type = 'I'
65                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
66                 WHEN hold_type = 'V'
67                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
68                 WHEN hold_type IN ('C','R','F')
69                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
70                 WHEN hold_type = 'M'
71                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
72                 WHEN hold_type = 'P'
73                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
74         END AS bib_record
75   FROM  action.hold_request ahr;
76
77 CREATE UNIQUE INDEX reporter_hold_request_record_pkey_idx ON reporter.hold_request_record (id);
78 CREATE INDEX reporter_hold_request_record_bib_record_idx ON reporter.hold_request_record (bib_record);
79
80 ALTER TABLE reporter.hold_request_record ADD PRIMARY KEY USING INDEX reporter_hold_request_record_pkey_idx;
81
82 CREATE FUNCTION reporter.hold_request_record_mapper () RETURNS TRIGGER AS $$
83 BEGIN
84     IF TG_OP = 'INSERT' THEN
85         INSERT INTO reporter.hold_request_record (id, target, hold_type, bib_record)
86         SELECT  NEW.id,
87                 NEW.target,
88                 NEW.hold_type,
89                 CASE
90                     WHEN NEW.hold_type = 'T'
91                         THEN NEW.target
92                     WHEN NEW.hold_type = 'I'
93                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
94                     WHEN NEW.hold_type = 'V'
95                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
96                     WHEN NEW.hold_type IN ('C','R','F')
97                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
98                     WHEN NEW.hold_type = 'M'
99                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
100                     WHEN NEW.hold_type = 'P'
101                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
102                 END AS bib_record;
103     ELSIF TG_OP = 'UPDATE' AND (OLD.target <> NEW.target OR OLD.hold_type <> NEW.hold_type) THEN
104         UPDATE  reporter.hold_request_record
105           SET   target = NEW.target,
106                 hold_type = NEW.hold_type,
107                 bib_record = CASE
108                     WHEN NEW.hold_type = 'T'
109                         THEN NEW.target
110                     WHEN NEW.hold_type = 'I'
111                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
112                     WHEN NEW.hold_type = 'V'
113                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
114                     WHEN NEW.hold_type IN ('C','R','F')
115                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
116                     WHEN NEW.hold_type = 'M'
117                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
118                     WHEN NEW.hold_type = 'P'
119                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
120                 END;
121     END IF;
122     RETURN NEW;
123 END;
124 $$ LANGUAGE PLPGSQL;
125
126 CREATE TRIGGER reporter_hold_request_record_trigger AFTER INSERT OR UPDATE ON action.hold_request
127     FOR EACH ROW EXECUTE PROCEDURE reporter.hold_request_record_mapper();
128
129 CREATE SCHEMA rating;
130
131 CREATE TABLE rating.popularity_parameter (
132     id          INT     PRIMARY KEY,
133     name        TEXT    NOT NULL UNIQUE, -- i18n
134     description TEXT,
135     func        TEXT,
136     require_horizon     BOOL    NOT NULL DEFAULT FALSE,
137     require_importance  BOOL    NOT NULL DEFAULT FALSE,
138     require_percentile  BOOL    NOT NULL DEFAULT FALSE
139 );
140
141 INSERT INTO rating.popularity_parameter (id,name,func,require_horizon,require_importance,require_percentile) VALUES
142     (1,'Holds Filled Over Time','rating.holds_filled_over_time',TRUE,FALSE,TRUE),
143     (2,'Holds Requested Over Time','rating.holds_placed_over_time',TRUE,FALSE,TRUE),
144     (3,'Current Hold Count','rating.current_hold_count',FALSE,FALSE,TRUE),
145     (4,'Circulations Over Time','rating.circs_over_time',TRUE,FALSE,TRUE),
146     (5,'Current Circulation Count','rating.current_circ_count',FALSE,FALSE,TRUE),
147     (6,'Out/Total Ratio','rating.checked_out_total_ratio',FALSE,FALSE,TRUE),
148     (7,'Holds/Total Ratio','rating.holds_total_ratio',FALSE,FALSE,TRUE),
149     (8,'Holds/Holdable Ratio','rating.holds_holdable_ratio',FALSE,FALSE,TRUE),
150     (9,'Percent of Time Circulating','rating.percent_time_circulating',FALSE,FALSE,TRUE),
151     (10,'Bibliographic Record Age (days, newer is better)','rating.bib_record_age',FALSE,FALSE,TRUE),
152     (11,'Publication Age (days, newer is better)','rating.bib_pub_age',FALSE,FALSE,TRUE),
153     (12,'On-line Bib has attributes','rating.generic_fixed_rating_by_uri',FALSE,FALSE,FALSE),
154     (13,'Bib has attributes and copies','rating.generic_fixed_rating_by_copy',FALSE,FALSE,FALSE),
155     (14,'Bib has attributes and copies or URIs','rating.generic_fixed_rating_by_copy_or_uri',FALSE,FALSE,FALSE),
156     (15,'Bib has attributes','rating.generic_fixed_rating_global',FALSE,FALSE,FALSE);
157
158 CREATE TABLE rating.badge (
159     id                      SERIAL      PRIMARY KEY,
160     name                    TEXT        NOT NULL,
161     description             TEXT,
162     scope                   INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
163     weight                  INT         NOT NULL DEFAULT 1,
164     horizon_age             INTERVAL,
165     importance_age          INTERVAL,
166     importance_interval     INTERVAL    NOT NULL DEFAULT '1 day',
167     importance_scale        NUMERIC     CHECK (importance_scale IS NULL OR importance_scale > 0.0),
168     recalc_interval         INTERVAL    NOT NULL DEFAULT '1 month',
169     attr_filter             TEXT,
170     src_filter              INT         REFERENCES config.bib_source (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
171     circ_mod_filter         TEXT        REFERENCES config.circ_modifier (code) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
172     loc_grp_filter          INT         REFERENCES asset.copy_location_group (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
173     popularity_parameter    INT         NOT NULL REFERENCES rating.popularity_parameter (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
174     fixed_rating            INT         CHECK (fixed_rating IS NULL OR fixed_rating BETWEEN -5 AND 5),
175     percentile              NUMERIC     CHECK (percentile IS NULL OR (percentile >= 50.0 AND percentile < 100.0)),
176     discard                 INT         NOT NULL DEFAULT 0, 
177     last_calc               TIMESTAMPTZ,
178     CONSTRAINT unique_name_scope UNIQUE (name,scope)
179 );
180
181 CREATE TABLE rating.record_badge_score (
182     id          BIGSERIAL   PRIMARY KEY,
183     record      BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
184     badge       INT         NOT NULL REFERENCES rating.badge (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
185     score       INT         NOT NULL CHECK (score BETWEEN -5 AND 5),
186     CONSTRAINT unique_record_badge UNIQUE (record,badge)
187 );
188 CREATE INDEX record_badge_score_badge_idx ON rating.record_badge_score (badge);
189 CREATE INDEX record_badge_score_record_idx ON rating.record_badge_score (record);
190
191 CREATE OR REPLACE VIEW rating.badge_with_orgs AS
192     WITH    org_scope AS (
193                 SELECT  id,
194                         array_agg(tree) AS orgs
195                   FROM  (SELECT id,
196                                 (actor.org_unit_descendants(id)).id AS tree
197                           FROM  actor.org_unit
198                         ) x
199                   GROUP BY 1
200             )
201     SELECT  b.*,
202             s.orgs
203       FROM  rating.badge b
204             JOIN org_scope s ON (b.scope = s.id);
205
206 CREATE OR REPLACE FUNCTION rating.precalc_src_filter(src INT)
207     RETURNS INT AS $f$
208 DECLARE
209     cnt     INT     := 0;
210 BEGIN
211
212     SET LOCAL client_min_messages = error;
213     DROP TABLE IF EXISTS precalc_src_filter_bib_list;
214     IF src IS NOT NULL THEN
215         CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
216             SELECT id FROM biblio.record_entry
217             WHERE source = src AND NOT deleted;
218     ELSE
219         CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
220             SELECT id FROM biblio.record_entry
221             WHERE id > 0 AND NOT deleted;
222     END IF;
223
224     SELECT count(*) INTO cnt FROM precalc_src_filter_bib_list;
225     RETURN cnt;
226 END;
227 $f$ LANGUAGE PLPGSQL;
228
229 CREATE OR REPLACE FUNCTION rating.precalc_circ_mod_filter(cm TEXT)
230     RETURNS INT AS $f$
231 DECLARE
232     cnt     INT     := 0;
233 BEGIN
234
235     SET LOCAL client_min_messages = error;
236     DROP TABLE IF EXISTS precalc_circ_mod_filter_bib_list;
237     IF cm IS NOT NULL THEN
238         CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
239             SELECT  cn.record AS id,
240                     cp.id AS copy
241               FROM  asset.call_number cn
242                     JOIN asset.copy cp ON (cn.id = cp.call_number)
243               WHERE cp.circ_modifier = cm
244                     AND NOT cp.deleted;
245     ELSE
246         CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
247             SELECT  cn.record AS id,
248                     cp.id AS copy
249               FROM  asset.call_number cn
250                     JOIN asset.copy cp ON (cn.id = cp.call_number)
251               WHERE NOT cp.deleted;
252     END IF;
253
254     SELECT count(*) INTO cnt FROM precalc_circ_mod_filter_bib_list;
255     RETURN cnt;
256 END;
257 $f$ LANGUAGE PLPGSQL;
258
259 CREATE OR REPLACE FUNCTION rating.precalc_location_filter(loc INT)
260     RETURNS INT AS $f$
261 DECLARE
262     cnt     INT     := 0;
263 BEGIN
264
265     SET LOCAL client_min_messages = error;
266     DROP TABLE IF EXISTS precalc_location_filter_bib_list;
267     IF loc IS NOT NULL THEN
268         CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
269             SELECT  cn.record AS id,
270                     cp.id AS copy
271               FROM  asset.call_number cn
272                     JOIN asset.copy cp ON (cn.id = cp.call_number)
273                     JOIN asset.copy_location_group_map lg ON (cp.location = lg.location)
274               WHERE lg.lgroup = loc
275                     AND NOT cp.deleted;
276     ELSE
277         CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
278             SELECT  cn.record AS id,
279                     cp.id AS copy
280               FROM  asset.call_number cn
281                     JOIN asset.copy cp ON (cn.id = cp.call_number)
282               WHERE NOT cp.deleted;
283     END IF;
284
285     SELECT count(*) INTO cnt FROM precalc_location_filter_bib_list;
286     RETURN cnt;
287 END;
288 $f$ LANGUAGE PLPGSQL;
289
290 -- all or limited...
291 CREATE OR REPLACE FUNCTION rating.precalc_attr_filter(attr_filter TEXT)
292     RETURNS INT AS $f$
293 DECLARE
294     cnt     INT := 0;
295     afilter TEXT;
296 BEGIN
297
298     SET LOCAL client_min_messages = error;
299     DROP TABLE IF EXISTS precalc_attr_filter_bib_list;
300     IF attr_filter IS NOT NULL THEN
301         afilter := metabib.compile_composite_attr(attr_filter);
302         CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
303             SELECT source AS id FROM metabib.record_attr_vector_list
304             WHERE vlist @@ metabib.compile_composite_attr(attr_filter);
305     ELSE
306         CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
307             SELECT source AS id FROM metabib.record_attr_vector_list;
308     END IF;
309
310     SELECT count(*) INTO cnt FROM precalc_attr_filter_bib_list;
311     RETURN cnt;
312 END;
313 $f$ LANGUAGE PLPGSQL;
314
315 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy(badge_id INT)
316     RETURNS INT AS $f$
317 DECLARE
318     cnt         INT     := 0;
319     badge_row   rating.badge_with_orgs%ROWTYPE;
320     base        TEXT;
321     whr         TEXT;
322 BEGIN
323
324     SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
325
326     SET LOCAL client_min_messages = error;
327     DROP TABLE IF EXISTS precalc_bibs_by_copy_list;
328     CREATE TEMP TABLE precalc_bibs_by_copy_list ON COMMIT DROP AS
329         SELECT  DISTINCT cn.record AS id
330           FROM  asset.call_number cn
331                 JOIN asset.copy cp ON (cp.call_number = cn.id AND NOT cp.deleted)
332                 JOIN precalc_copy_filter_bib_list f ON (cp.id = f.copy)
333           WHERE cn.owning_lib = ANY (badge_row.orgs)
334                 AND NOT cn.deleted;
335
336     SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_list;
337     RETURN cnt;
338 END;
339 $f$ LANGUAGE PLPGSQL;
340
341 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_uri(badge_id INT)
342     RETURNS INT AS $f$
343 DECLARE
344     cnt         INT     := 0;
345     badge_row   rating.badge_with_orgs%ROWTYPE;
346 BEGIN
347
348     SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
349
350     SET LOCAL client_min_messages = error;
351     DROP TABLE IF EXISTS precalc_bibs_by_uri_list;
352     CREATE TEMP TABLE precalc_bibs_by_uri_list ON COMMIT DROP AS
353         SELECT  DISTINCT record AS id
354           FROM  asset.call_number cn
355                 JOIN asset.uri_call_number_map urim ON (urim.call_number = cn.id)
356                 JOIN asset.uri uri ON (urim.uri = uri.id AND uri.active)
357           WHERE cn.owning_lib = ANY (badge_row.orgs)
358                 AND cn.label = '##URI##'
359                 AND NOT cn.deleted;
360
361     SELECT count(*) INTO cnt FROM precalc_bibs_by_uri_list;
362     RETURN cnt;
363 END;
364 $f$ LANGUAGE PLPGSQL;
365
366 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy_or_uri(badge_id INT)
367     RETURNS INT AS $f$
368 DECLARE
369     cnt         INT     := 0;
370 BEGIN
371
372     PERFORM rating.precalc_bibs_by_copy(badge_id);
373     PERFORM rating.precalc_bibs_by_uri(badge_id);
374
375     SET LOCAL client_min_messages = error;
376     DROP TABLE IF EXISTS precalc_bibs_by_copy_or_uri_list;
377     CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list ON COMMIT DROP AS
378         SELECT id FROM precalc_bibs_by_copy_list
379             UNION
380         SELECT id FROM precalc_bibs_by_uri_list;
381
382     SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_or_uri_list;
383     RETURN cnt;
384 END;
385 $f$ LANGUAGE PLPGSQL;
386
387
388 CREATE OR REPLACE FUNCTION rating.recalculate_badge_score ( badge_id INT, setup_only BOOL DEFAULT FALSE ) RETURNS VOID AS $f$
389 DECLARE
390     badge_row           rating.badge%ROWTYPE;
391     param           rating.popularity_parameter%ROWTYPE;
392 BEGIN
393     SET LOCAL client_min_messages = error;
394
395     -- Find what we're doing    
396     SELECT * INTO badge_row FROM rating.badge WHERE id = badge_id;
397     SELECT * INTO param FROM rating.popularity_parameter WHERE id = badge_row.popularity_parameter;
398
399     -- Calculate the filtered bib set, or all bibs if none
400     PERFORM rating.precalc_attr_filter(badge_row.attr_filter);
401     PERFORM rating.precalc_src_filter(badge_row.src_filter);
402     PERFORM rating.precalc_circ_mod_filter(badge_row.circ_mod_filter);
403     PERFORM rating.precalc_location_filter(badge_row.loc_grp_filter);
404
405     -- Bring the bib-level filter lists together
406     DROP TABLE IF EXISTS precalc_bib_filter_bib_list;
407     CREATE TEMP TABLE precalc_bib_filter_bib_list ON COMMIT DROP AS
408         SELECT id FROM precalc_attr_filter_bib_list
409             INTERSECT
410         SELECT id FROM precalc_src_filter_bib_list;
411
412     -- Bring the copy-level filter lists together. We're keeping this for bib_by_copy filtering later.
413     DROP TABLE IF EXISTS precalc_copy_filter_bib_list;
414     CREATE TEMP TABLE precalc_copy_filter_bib_list ON COMMIT DROP AS
415         SELECT id, copy FROM precalc_circ_mod_filter_bib_list
416             INTERSECT
417         SELECT id, copy FROM precalc_location_filter_bib_list;
418
419     -- Bring the collapsed filter lists together
420     DROP TABLE IF EXISTS precalc_filter_bib_list;
421     CREATE TEMP TABLE precalc_filter_bib_list ON COMMIT DROP AS
422         SELECT id FROM precalc_bib_filter_bib_list
423             INTERSECT
424         SELECT id FROM precalc_copy_filter_bib_list;
425
426     CREATE INDEX precalc_filter_bib_list_idx
427         ON precalc_filter_bib_list (id);
428
429     IF setup_only THEN
430         RETURN;
431     END IF;
432
433     -- If it's a fixed-rating badge, just do it ...
434     IF badge_row.fixed_rating IS NOT NULL THEN
435         DELETE FROM rating.record_badge_score WHERE badge = badge_id;
436         EXECUTE $e$
437             INSERT INTO rating.record_badge_score (record, badge, score)
438                 SELECT record, $1, $2 FROM $e$ || param.func || $e$($1)$e$
439         USING badge_id, badge_row.fixed_rating;
440
441         UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
442
443         RETURN;
444     END IF;
445     -- else, calculate!
446
447     -- Make a session-local scratchpad for calculating scores
448     CREATE TEMP TABLE record_score_scratchpad (
449         bib     BIGINT,
450         value   NUMERIC
451     ) ON COMMIT DROP;
452
453     -- Gather raw values
454     EXECUTE $e$
455         INSERT INTO record_score_scratchpad (bib, value)
456             SELECT * FROM $e$ || param.func || $e$($1)$e$
457     USING badge_id;
458
459     IF badge_row.discard > 0 OR badge_row.percentile IS NOT NULL THEN
460         -- To speed up discard-common
461         CREATE INDEX record_score_scratchpad_score_idx ON record_score_scratchpad (value);
462         ANALYZE record_score_scratchpad;
463     END IF;
464
465     IF badge_row.discard > 0 THEN -- Remove common low values (trim the long tail)
466         DELETE FROM record_score_scratchpad WHERE value IN (
467             SELECT DISTINCT value FROM record_score_scratchpad ORDER BY value LIMIT badge_row.discard
468         );
469     END IF;
470
471     IF badge_row.percentile IS NOT NULL THEN -- Cut population down to exceptional records
472         DELETE FROM record_score_scratchpad WHERE value <= (
473             SELECT value FROM (
474                 SELECT  value,
475                         CUME_DIST() OVER (ORDER BY value) AS p
476                   FROM  record_score_scratchpad
477             ) x WHERE p < badge_row.percentile / 100.0 ORDER BY p DESC LIMIT 1
478         );
479     END IF;
480
481
482     -- And, finally, push new data in
483     DELETE FROM rating.record_badge_score WHERE badge = badge_id;
484     INSERT INTO rating.record_badge_score (badge, record, score)
485         SELECT  badge_id,
486                 bib,
487                 GREATEST(ROUND((CUME_DIST() OVER (ORDER BY value)) * 5), 1) AS value
488           FROM  record_score_scratchpad;
489
490     DROP TABLE record_score_scratchpad;
491
492     -- Now, finally-finally, mark the badge as recalculated
493     UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
494
495     RETURN;
496 END;
497 $f$ LANGUAGE PLPGSQL;
498
499 CREATE OR REPLACE FUNCTION rating.holds_filled_over_time(badge_id INT)
500     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
501 DECLARE
502     badge   rating.badge_with_orgs%ROWTYPE;
503     iage    INT     := 1;
504     iint    INT     := NULL;
505     iscale  NUMERIC := NULL;
506 BEGIN
507
508     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
509
510     IF badge.horizon_age IS NULL THEN
511         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
512             badge.name,
513             badge.id;
514     END IF;
515
516     PERFORM rating.precalc_bibs_by_copy(badge_id);
517
518     SET LOCAL client_min_messages = error;
519     DROP TABLE IF EXISTS precalc_bib_list;
520     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
521         SELECT id FROM precalc_filter_bib_list
522             INTERSECT
523         SELECT id FROM precalc_bibs_by_copy_list;
524
525     iint := EXTRACT(EPOCH FROM badge.importance_interval);
526     IF badge.importance_age IS NOT NULL THEN
527         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
528     END IF;
529
530     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
531     iscale := COALESCE(badge.importance_scale, 1.0);
532
533     RETURN QUERY
534      SELECT bib,
535             SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
536       FROM (
537          SELECT f.id AS bib,
538                 (1 + EXTRACT(EPOCH FROM AGE(h.fulfillment_time)) / iint)::INT AS hage,
539                 COUNT(h.id)::INT AS holds
540           FROM  action.hold_request h
541                 JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
542                 JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
543           WHERE h.fulfillment_time >= NOW() - badge.horizon_age
544                 AND h.request_lib = ANY (badge.orgs)
545           GROUP BY 1, 2
546       ) x
547       GROUP BY 1;
548 END;
549 $f$ LANGUAGE PLPGSQL STRICT;
550
551 CREATE OR REPLACE FUNCTION rating.holds_placed_over_time(badge_id INT)
552     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
553 DECLARE
554     badge   rating.badge_with_orgs%ROWTYPE;
555     iage    INT     := 1;
556     iint    INT     := NULL;
557     iscale  NUMERIC := NULL;
558 BEGIN
559
560     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
561
562     IF badge.horizon_age IS NULL THEN
563         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
564             badge.name,
565             badge.id;
566     END IF;
567
568     PERFORM rating.precalc_bibs_by_copy(badge_id);
569
570     SET LOCAL client_min_messages = error;
571     DROP TABLE IF EXISTS precalc_bib_list;
572     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
573         SELECT id FROM precalc_filter_bib_list
574             INTERSECT
575         SELECT id FROM precalc_bibs_by_copy_list;
576
577     iint := EXTRACT(EPOCH FROM badge.importance_interval);
578     IF badge.importance_age IS NOT NULL THEN
579         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
580     END IF;
581
582     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
583     iscale := COALESCE(badge.importance_scale, 1.0);
584
585     RETURN QUERY
586      SELECT bib,
587             SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
588       FROM (
589          SELECT f.id AS bib,
590                 (1 + EXTRACT(EPOCH FROM AGE(h.request_time)) / iint)::INT AS hage,
591                 COUNT(h.id)::INT AS holds
592           FROM  action.hold_request h
593                 JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
594                 JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
595           WHERE h.request_time >= NOW() - badge.horizon_age
596                 AND h.request_lib = ANY (badge.orgs)
597           GROUP BY 1, 2
598       ) x
599       GROUP BY 1;
600 END;
601 $f$ LANGUAGE PLPGSQL STRICT;
602
603 CREATE OR REPLACE FUNCTION rating.current_hold_count(badge_id INT)
604     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
605 DECLARE
606     badge   rating.badge_with_orgs%ROWTYPE;
607 BEGIN
608
609     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
610
611     PERFORM rating.precalc_bibs_by_copy(badge_id);
612
613     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
614         SELECT id FROM precalc_filter_bib_list
615             INTERSECT
616         SELECT id FROM precalc_bibs_by_copy_list
617     );
618
619     ANALYZE precalc_copy_filter_bib_list;
620
621     RETURN QUERY
622      SELECT rhrr.bib_record AS bib,
623             COUNT(DISTINCT h.id)::NUMERIC AS holds
624       FROM  action.hold_request h
625             JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
626             JOIN action.hold_copy_map m ON (m.hold = h.id)
627             JOIN precalc_copy_filter_bib_list cf ON (rhrr.bib_record = cf.id AND m.target_copy = cf.copy)
628       WHERE h.fulfillment_time IS NULL
629             AND h.request_lib = ANY (badge.orgs)
630       GROUP BY 1;
631 END;
632 $f$ LANGUAGE PLPGSQL STRICT;
633
634 CREATE OR REPLACE FUNCTION rating.circs_over_time(badge_id INT)
635     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
636 DECLARE
637     badge   rating.badge_with_orgs%ROWTYPE;
638     iage    INT     := 1;
639     iint    INT     := NULL;
640     iscale  NUMERIC := NULL;
641 BEGIN
642
643     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
644
645     IF badge.horizon_age IS NULL THEN
646         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
647             badge.name,
648             badge.id;
649     END IF;
650
651     PERFORM rating.precalc_bibs_by_copy(badge_id);
652
653     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
654         SELECT id FROM precalc_filter_bib_list
655             INTERSECT
656         SELECT id FROM precalc_bibs_by_copy_list
657     );
658
659     ANALYZE precalc_copy_filter_bib_list;
660
661     iint := EXTRACT(EPOCH FROM badge.importance_interval);
662     IF badge.importance_age IS NOT NULL THEN
663         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
664     END IF;
665
666     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
667     iscale := COALESCE(badge.importance_scale, 1.0);
668
669     RETURN QUERY
670      SELECT bib,
671             SUM( circs * GREATEST( iscale * (iage - cage), 1.0 ))
672       FROM (
673          SELECT cn.record AS bib,
674                 (1 + EXTRACT(EPOCH FROM AGE(c.xact_start)) / iint)::INT AS cage,
675                 COUNT(c.id)::INT AS circs
676           FROM  action.circulation c
677                 JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
678                 JOIN asset.copy cp ON (cp.id = c.target_copy)
679                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
680           WHERE c.xact_start >= NOW() - badge.horizon_age
681                 AND cn.owning_lib = ANY (badge.orgs)
682                 AND c.phone_renewal IS FALSE  -- we don't count renewals
683                 AND c.desk_renewal IS FALSE
684                 AND c.opac_renewal IS FALSE
685           GROUP BY 1, 2
686       ) x
687       GROUP BY 1;
688 END;
689 $f$ LANGUAGE PLPGSQL STRICT;
690
691 CREATE OR REPLACE FUNCTION rating.current_circ_count(badge_id INT)
692     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
693 DECLARE
694     badge   rating.badge_with_orgs%ROWTYPE;
695 BEGIN
696
697     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
698
699     PERFORM rating.precalc_bibs_by_copy(badge_id);
700
701     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
702         SELECT id FROM precalc_filter_bib_list
703             INTERSECT
704         SELECT id FROM precalc_bibs_by_copy_list
705     );
706
707     ANALYZE precalc_copy_filter_bib_list;
708
709     RETURN QUERY
710      SELECT cn.record AS bib,
711             COUNT(c.id)::NUMERIC AS circs
712       FROM  action.circulation c
713             JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
714             JOIN asset.copy cp ON (cp.id = c.target_copy)
715             JOIN asset.call_number cn ON (cn.id = cp.call_number)
716       WHERE c.checkin_time IS NULL
717             AND cn.owning_lib = ANY (badge.orgs)
718       GROUP BY 1;
719
720 END;
721 $f$ LANGUAGE PLPGSQL STRICT;
722
723 CREATE OR REPLACE FUNCTION rating.checked_out_total_ratio(badge_id INT)
724     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
725 DECLARE
726     badge   rating.badge_with_orgs%ROWTYPE;
727 BEGIN
728
729     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
730
731     PERFORM rating.precalc_bibs_by_copy(badge_id);
732
733     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
734         SELECT id FROM precalc_filter_bib_list
735             INTERSECT
736         SELECT id FROM precalc_bibs_by_copy_list
737     );
738
739     ANALYZE precalc_copy_filter_bib_list;
740
741     RETURN QUERY
742      SELECT bib,
743             SUM(checked_out)::NUMERIC / SUM(total)::NUMERIC
744       FROM  (SELECT cn.record AS bib,
745                     (cp.status = 1)::INT AS checked_out,
746                     1 AS total
747               FROM  asset.copy cp
748                     JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
749                     JOIN asset.call_number cn ON (cn.id = cp.call_number)
750               WHERE cn.owning_lib = ANY (badge.orgs)
751             ) x
752       GROUP BY 1;
753 END;
754 $f$ LANGUAGE PLPGSQL STRICT;
755
756 CREATE OR REPLACE FUNCTION rating.holds_total_ratio(badge_id INT)
757     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
758 DECLARE
759     badge   rating.badge_with_orgs%ROWTYPE;
760 BEGIN
761
762     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
763
764     PERFORM rating.precalc_bibs_by_copy(badge_id);
765
766     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
767         SELECT id FROM precalc_filter_bib_list
768             INTERSECT
769         SELECT id FROM precalc_bibs_by_copy_list
770     );
771
772     ANALYZE precalc_copy_filter_bib_list;
773
774     RETURN QUERY
775      SELECT cn.record AS bib,
776             COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
777       FROM  asset.copy cp
778             JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
779             JOIN asset.call_number cn ON (cn.id = cp.call_number)
780             JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
781       WHERE cn.owning_lib = ANY (badge.orgs)
782       GROUP BY 1;
783 END;
784 $f$ LANGUAGE PLPGSQL STRICT;
785
786 CREATE OR REPLACE FUNCTION rating.holds_holdable_ratio(badge_id INT)
787     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
788 DECLARE
789     badge   rating.badge_with_orgs%ROWTYPE;
790 BEGIN
791
792     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
793
794     PERFORM rating.precalc_bibs_by_copy(badge_id);
795
796     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
797         SELECT id FROM precalc_filter_bib_list
798             INTERSECT
799         SELECT id FROM precalc_bibs_by_copy_list
800     );
801
802     ANALYZE precalc_copy_filter_bib_list;
803
804     RETURN QUERY
805      SELECT cn.record AS bib,
806             COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
807       FROM  asset.copy cp
808             JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
809             JOIN asset.copy_location cl ON (cl.id = cp.location)
810             JOIN config.copy_status cs ON (cs.id = cp.status)
811             JOIN asset.call_number cn ON (cn.id = cp.call_number)
812             JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
813       WHERE cn.owning_lib = ANY (badge.orgs)
814             AND cp.holdable IS TRUE
815             AND cl.holdable IS TRUE
816             AND cs.holdable IS TRUE
817       GROUP BY 1;
818 END;
819 $f$ LANGUAGE PLPGSQL STRICT;
820
821 CREATE OR REPLACE FUNCTION rating.bib_record_age(badge_id INT)
822     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
823 DECLARE
824     badge   rating.badge_with_orgs%ROWTYPE;
825 BEGIN
826
827     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
828
829     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
830
831     SET LOCAL client_min_messages = error;
832     DROP TABLE IF EXISTS precalc_bib_list;
833     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
834         SELECT id FROM precalc_filter_bib_list
835             INTERSECT
836         SELECT id FROM precalc_bibs_by_copy_or_uri_list;
837
838     RETURN QUERY
839      SELECT b.id,
840             1.0 / EXTRACT(EPOCH FROM AGE(b.create_date))::NUMERIC + 1.0
841       FROM  precalc_bib_list pop
842             JOIN biblio.record_entry b ON (b.id = pop.id);
843 END;
844 $f$ LANGUAGE PLPGSQL STRICT;
845
846 CREATE OR REPLACE FUNCTION rating.bib_pub_age(badge_id INT)
847     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
848 DECLARE
849     badge   rating.badge_with_orgs%ROWTYPE;
850 BEGIN
851
852     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
853
854     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
855
856     SET LOCAL client_min_messages = error;
857     DROP TABLE IF EXISTS precalc_bib_list;
858     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
859         SELECT id FROM precalc_filter_bib_list
860             INTERSECT
861         SELECT id FROM precalc_bibs_by_copy_or_uri_list;
862
863     RETURN QUERY
864      SELECT pop.id AS bib,
865             s.value::NUMERIC
866       FROM  precalc_bib_list pop
867             JOIN metabib.record_sorter s ON (
868                 s.source = pop.id
869                 AND s.attr = 'pubdate'
870                 AND s.value ~ '^\d+$'
871             )
872       WHERE s.value::INT <= EXTRACT(YEAR FROM NOW())::INT;
873 END;
874 $f$ LANGUAGE PLPGSQL STRICT;
875
876 CREATE OR REPLACE FUNCTION rating.percent_time_circulating(badge_id INT)
877     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
878 DECLARE
879     badge   rating.badge_with_orgs%ROWTYPE;
880 BEGIN
881
882     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
883
884     PERFORM rating.precalc_bibs_by_copy(badge_id);
885
886     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
887         SELECT id FROM precalc_filter_bib_list
888             INTERSECT
889         SELECT id FROM precalc_bibs_by_copy_list
890     );
891
892     ANALYZE precalc_copy_filter_bib_list;
893
894     RETURN QUERY
895      SELECT bib,
896             SUM(COALESCE(circ_time,0))::NUMERIC / SUM(age)::NUMERIC
897       FROM  (SELECT cn.record AS bib,
898                     cp.id,
899                     EXTRACT( EPOCH FROM AGE(cp.active_date) ) + 1 AS age,
900                     SUM(  -- time copy spent circulating
901                         EXTRACT(
902                             EPOCH FROM
903                             AGE(
904                                 COALESCE(circ.checkin_time, circ.stop_fines_time, NOW()),
905                                 circ.xact_start
906                             )
907                         )
908                     )::NUMERIC AS circ_time
909               FROM  asset.copy cp
910                     JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
911                     JOIN asset.call_number cn ON (cn.id = cp.call_number)
912                     LEFT JOIN action.all_circulation circ ON (
913                         circ.target_copy = cp.id
914                         AND stop_fines NOT IN (
915                             'LOST',
916                             'LONGOVERDUE',
917                             'CLAIMSRETURNED',
918                             'LONGOVERDUE'
919                         )
920                         AND NOT (
921                             checkin_time IS NULL AND
922                             stop_fines = 'MAXFINES'
923                         )
924                     )
925               WHERE cn.owning_lib = ANY (badge.orgs)
926                     AND cp.active_date IS NOT NULL
927                     -- Next line requires that copies with no circs (circ.id IS NULL) also not be deleted
928                     AND ((circ.id IS NULL AND NOT cp.deleted) OR circ.id IS NOT NULL)
929               GROUP BY 1,2,3
930             ) x
931       GROUP BY 1;
932 END;
933 $f$ LANGUAGE PLPGSQL STRICT;
934
935 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy(badge_id INT)
936     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
937 BEGIN
938     PERFORM rating.precalc_bibs_by_copy(badge_id);
939     RETURN QUERY
940         SELECT id, 1.0 FROM precalc_filter_bib_list
941             INTERSECT
942         SELECT id, 1.0 FROM precalc_bibs_by_copy_list;
943 END;
944 $f$ LANGUAGE PLPGSQL STRICT;
945
946 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_uri(badge_id INT)
947     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
948 BEGIN
949     PERFORM rating.precalc_bibs_by_uri(badge_id);
950     RETURN QUERY
951         SELECT id, 1.0 FROM precalc_bib_filter_bib_list
952             INTERSECT
953         SELECT id, 1.0 FROM precalc_bibs_by_uri_list;
954 END;
955 $f$ LANGUAGE PLPGSQL STRICT;
956
957 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy_or_uri(badge_id INT)
958     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
959 BEGIN
960     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
961     RETURN QUERY
962         (SELECT id, 1.0 FROM precalc_filter_bib_list
963             INTERSECT
964         SELECT id, 1.0 FROM precalc_bibs_by_copy_list)
965             UNION
966         (SELECT id, 1.0 FROM precalc_bib_filter_bib_list
967             INTERSECT
968         SELECT id, 1.0 FROM precalc_bibs_by_uri_list);
969 END;
970 $f$ LANGUAGE PLPGSQL STRICT;
971
972 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_global(badge_id INT)
973     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
974 BEGIN
975     RETURN QUERY
976         SELECT id, 1.0 FROM precalc_bib_filter_bib_list;
977 END;
978 $f$ LANGUAGE PLPGSQL STRICT;
979
980 CREATE INDEX hold_fulfillment_time_idx ON action.hold_request (fulfillment_time) WHERE fulfillment_time IS NOT NULL;
981 CREATE INDEX hold_request_time_idx ON action.hold_request (request_time);
982
983 COMMIT;
984