pg_statistics_helpers.sql (19521B)
1 -- 2 -- This file is part of TALER 3 -- Copyright (C) 2025 Taler Systems SA 4 -- 5 -- TALER is free software; you can redistribute it and/or modify it under the 6 -- terms of the GNU General Public License as published by the Free Software 7 -- Foundation; either version 3, or (at your option) any later version. 8 -- 9 -- TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 -- A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 -- 13 -- You should have received a copy of the GNU General Public License along with 14 -- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 -- 16 17 18 DROP PROCEDURE IF EXISTS merchant_do_bump_number_bucket_stat; 19 CREATE OR REPLACE PROCEDURE merchant_do_bump_number_bucket_stat( 20 in_slug TEXT, 21 in_timestamp TIMESTAMP, 22 in_delta INT8 23 ) 24 LANGUAGE plpgsql 25 AS $$ 26 DECLARE 27 my_meta INT8; 28 my_range merchant.statistic_range; 29 my_bucket_start INT8; 30 my_curs CURSOR (arg_slug TEXT) 31 FOR SELECT UNNEST(ranges) 32 FROM merchant_statistic_bucket_meta 33 WHERE slug=arg_slug; 34 BEGIN 35 SELECT bmeta_serial_id 36 INTO my_meta 37 FROM merchant_statistic_bucket_meta 38 WHERE slug=in_slug 39 AND stype='number'; 40 IF NOT FOUND 41 THEN 42 RETURN; 43 END IF; 44 OPEN my_curs (arg_slug:=in_slug); 45 LOOP 46 FETCH NEXT 47 FROM my_curs 48 INTO my_range; 49 EXIT WHEN NOT FOUND; 50 SELECT * 51 INTO my_bucket_start 52 FROM merchant.interval_to_start (in_timestamp, my_range); 53 54 UPDATE merchant_statistic_bucket_counter 55 SET cumulative_number = cumulative_number + in_delta 56 WHERE bmeta_serial_id=my_meta 57 AND bucket_start=my_bucket_start 58 AND bucket_range=my_range; 59 IF NOT FOUND 60 THEN 61 INSERT INTO merchant_statistic_bucket_counter 62 (bmeta_serial_id 63 ,bucket_start 64 ,bucket_range 65 ,cumulative_number 66 ) VALUES ( 67 my_meta 68 ,my_bucket_start 69 ,my_range 70 ,in_delta); 71 END IF; 72 END LOOP; 73 CLOSE my_curs; 74 END $$; 75 76 77 DROP PROCEDURE IF EXISTS merchant_do_bump_amount_bucket_stat; 78 CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_bucket_stat( 79 in_slug TEXT, 80 in_timestamp TIMESTAMP, 81 in_delta merchant.taler_amount_currency 82 ) 83 LANGUAGE plpgsql 84 AS $$ 85 DECLARE 86 my_meta INT8; 87 my_range merchant.statistic_range; 88 my_bucket_start INT8; 89 my_curs CURSOR (arg_slug TEXT) 90 FOR SELECT UNNEST(ranges) 91 FROM merchant_statistic_bucket_meta 92 WHERE slug=arg_slug; 93 BEGIN 94 SELECT bmeta_serial_id 95 INTO my_meta 96 FROM merchant_statistic_bucket_meta 97 WHERE slug=in_slug 98 AND stype='amount'; 99 IF NOT FOUND 100 THEN 101 RETURN; 102 END IF; 103 OPEN my_curs (arg_slug:=in_slug); 104 LOOP 105 FETCH NEXT 106 FROM my_curs 107 INTO my_range; 108 EXIT WHEN NOT FOUND; 109 SELECT * 110 INTO my_bucket_start 111 FROM merchant.interval_to_start (in_timestamp, my_range); 112 113 UPDATE merchant_statistic_bucket_amount 114 SET 115 cumulative_value = cumulative_value + (in_delta).val 116 + CASE 117 WHEN (in_delta).frac + cumulative_frac >= 100000000 118 THEN 1 119 ELSE 0 120 END, 121 cumulative_frac = cumulative_frac + (in_delta).frac 122 - CASE 123 WHEN (in_delta).frac + cumulative_frac >= 100000000 124 THEN 100000000 125 ELSE 0 126 END 127 WHERE bmeta_serial_id=my_meta 128 AND curr=(in_delta).curr 129 AND bucket_start=my_bucket_start 130 AND bucket_range=my_range; 131 IF NOT FOUND 132 THEN 133 INSERT INTO merchant_statistic_bucket_amount 134 (bmeta_serial_id 135 ,bucket_start 136 ,bucket_range 137 ,curr 138 ,cumulative_value 139 ,cumulative_frac 140 ) VALUES ( 141 my_meta 142 ,my_bucket_start 143 ,my_range 144 ,(in_delta).curr 145 ,(in_delta).val 146 ,(in_delta).frac); 147 END IF; 148 END LOOP; 149 CLOSE my_curs; 150 END $$; 151 152 COMMENT ON PROCEDURE merchant_do_bump_amount_bucket_stat 153 IS 'Updates an amount statistic tracked over buckets'; 154 155 156 DROP PROCEDURE IF EXISTS merchant_do_bump_number_interval_stat; 157 CREATE OR REPLACE PROCEDURE merchant_do_bump_number_interval_stat( 158 in_slug TEXT, 159 in_timestamp TIMESTAMP, 160 in_delta INT8 161 ) 162 LANGUAGE plpgsql 163 AS $$ 164 DECLARE 165 my_now INT8; 166 my_record RECORD; 167 my_meta INT8; 168 my_ranges INT8[]; 169 my_precisions INT8[]; 170 my_rangex INT8; 171 my_precisionx INT8; 172 my_start INT8; 173 my_event INT8; 174 BEGIN 175 my_now = ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; 176 SELECT imeta_serial_id 177 ,ranges AS ranges 178 ,precisions AS precisions 179 INTO my_record 180 FROM merchant_statistic_interval_meta 181 WHERE slug=in_slug 182 AND stype='number'; 183 IF NOT FOUND 184 THEN 185 RETURN; 186 END IF; 187 188 my_start = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8 / 1000 / 1000; -- convert to seconds 189 my_precisions = my_record.precisions; 190 my_ranges = my_record.ranges; 191 my_rangex = NULL; 192 FOR my_x IN 1..COALESCE(array_length(my_ranges,1),0) 193 LOOP 194 IF my_now - my_ranges[my_x] < my_start 195 THEN 196 my_rangex = my_ranges[my_x]; 197 my_precisionx = my_precisions[my_x]; 198 EXIT; 199 END IF; 200 END LOOP; 201 IF my_rangex IS NULL 202 THEN 203 -- event is beyond the ranges we care about 204 RETURN; 205 END IF; 206 207 my_meta = my_record.imeta_serial_id; 208 my_start = my_start - my_start % my_precisionx; -- round down 209 210 INSERT INTO merchant_statistic_counter_event AS msce 211 (imeta_serial_id 212 ,slot 213 ,delta) 214 VALUES 215 (my_meta 216 ,my_start 217 ,in_delta) 218 ON CONFLICT (imeta_serial_id, slot) 219 DO UPDATE SET 220 delta = msce.delta + in_delta 221 RETURNING nevent_serial_id 222 INTO my_event; 223 224 UPDATE merchant_statistic_interval_counter 225 SET cumulative_number = cumulative_number + in_delta 226 WHERE imeta_serial_id = my_meta 227 AND range=my_rangex; 228 IF NOT FOUND 229 THEN 230 INSERT INTO merchant_statistic_interval_counter 231 (imeta_serial_id 232 ,range 233 ,event_delimiter 234 ,cumulative_number 235 ) VALUES ( 236 my_meta 237 ,my_rangex 238 ,my_event 239 ,in_delta); 240 END IF; 241 END $$; 242 243 COMMENT ON PROCEDURE merchant_do_bump_number_interval_stat 244 IS 'Updates a numeric statistic tracked over an interval'; 245 246 247 DROP PROCEDURE IF EXISTS merchant_do_bump_amount_interval_stat; 248 CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_interval_stat( 249 in_slug TEXT, 250 in_timestamp TIMESTAMP, 251 in_delta merchant.taler_amount_currency -- new amount in table that we should add to the tracker 252 ) 253 LANGUAGE plpgsql 254 AS $$ 255 DECLARE 256 my_now INT8; 257 my_record RECORD; 258 my_meta INT8; 259 my_ranges INT8[]; 260 my_precisions INT8[]; 261 my_x INT; 262 my_rangex INT8; 263 my_precisionx INT8; 264 my_start INT8; 265 my_event INT8; 266 BEGIN 267 my_now = ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; 268 SELECT imeta_serial_id 269 ,ranges 270 ,precisions 271 INTO my_record 272 FROM merchant_statistic_interval_meta 273 WHERE slug=in_slug 274 AND stype='amount'; 275 IF NOT FOUND 276 THEN 277 RETURN; 278 END IF; 279 280 my_start = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8 / 1000 / 1000; -- convert to seconds since epoch 281 my_precisions = my_record.precisions; 282 my_ranges = my_record.ranges; 283 my_rangex = NULL; 284 FOR my_x IN 1..COALESCE(array_length(my_ranges,1),0) 285 LOOP 286 IF my_now - my_ranges[my_x] < my_start 287 THEN 288 my_rangex = my_ranges[my_x]; 289 my_precisionx = my_precisions[my_x]; 290 EXIT; 291 END IF; 292 END LOOP; 293 IF my_rangex IS NULL 294 THEN 295 -- event is beyond the ranges we care about 296 RETURN; 297 END IF; 298 my_start = my_start - my_start % my_precisionx; -- round down 299 my_meta = my_record.imeta_serial_id; 300 301 INSERT INTO merchant_statistic_amount_event AS msae 302 (imeta_serial_id 303 ,slot 304 ,delta_curr 305 ,delta_value 306 ,delta_frac 307 ) VALUES ( 308 my_meta 309 ,my_start 310 ,(in_delta).curr 311 ,(in_delta).val 312 ,(in_delta).frac 313 ) 314 ON CONFLICT (imeta_serial_id, slot, delta_curr) 315 DO UPDATE SET 316 delta_value = msae.delta_value + (in_delta).val 317 + CASE 318 WHEN (in_delta).frac + msae.delta_frac >= 100000000 319 THEN 1 320 ELSE 0 321 END, 322 delta_frac = msae.delta_frac + (in_delta).frac 323 - CASE 324 WHEN (in_delta).frac + msae.delta_frac >= 100000000 325 THEN 100000000 326 ELSE 0 327 END 328 RETURNING aevent_serial_id 329 INTO my_event; 330 331 UPDATE merchant_statistic_interval_amount 332 SET 333 cumulative_value = cumulative_value + (in_delta).val 334 + CASE 335 WHEN (in_delta).frac + cumulative_frac >= 100000000 336 THEN 1 337 ELSE 0 338 END, 339 cumulative_frac = cumulative_frac + (in_delta).frac 340 - CASE 341 WHEN (in_delta).frac + cumulative_frac >= 100000000 342 THEN 100000000 343 ELSE 0 344 END 345 WHERE imeta_serial_id=my_meta 346 AND range=my_rangex 347 AND curr=(in_delta).curr; 348 IF NOT FOUND 349 THEN 350 INSERT INTO merchant_statistic_interval_amount 351 (imeta_serial_id 352 ,range 353 ,event_delimiter 354 ,curr 355 ,cumulative_value 356 ,cumulative_frac 357 ) VALUES ( 358 my_meta 359 ,my_rangex 360 ,my_event 361 ,(in_delta).curr 362 ,(in_delta).val 363 ,(in_delta).frac); 364 END IF; 365 END $$; 366 COMMENT ON PROCEDURE merchant_do_bump_amount_interval_stat 367 IS 'Updates an amount statistic tracked over an interval'; 368 369 370 DROP PROCEDURE IF EXISTS merchant_do_bump_number_stat; 371 CREATE OR REPLACE PROCEDURE merchant_do_bump_number_stat( 372 in_slug TEXT, 373 in_timestamp TIMESTAMP, 374 in_delta INT8 375 ) 376 LANGUAGE plpgsql 377 AS $$ 378 BEGIN 379 CALL merchant_do_bump_number_bucket_stat (in_slug, in_timestamp, in_delta); 380 CALL merchant_do_bump_number_interval_stat (in_slug, in_timestamp, in_delta); 381 END $$; 382 COMMENT ON PROCEDURE merchant_do_bump_number_stat 383 IS 'Updates a numeric statistic (bucket or interval)'; 384 385 386 DROP PROCEDURE IF EXISTS merchant_do_bump_amount_stat; 387 CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_stat( 388 in_slug TEXT, 389 in_timestamp TIMESTAMP, 390 in_delta merchant.taler_amount_currency 391 ) 392 LANGUAGE plpgsql 393 AS $$ 394 BEGIN 395 CALL merchant_do_bump_amount_bucket_stat (in_slug, in_timestamp, in_delta); 396 CALL merchant_do_bump_amount_interval_stat (in_slug, in_timestamp, in_delta); 397 END $$; 398 COMMENT ON PROCEDURE merchant_do_bump_amount_stat 399 IS 'Updates an amount statistic (bucket or interval)'; 400 401 402 403 DROP PROCEDURE IF EXISTS merchant_statistic_counter_gc; 404 CREATE OR REPLACE PROCEDURE merchant_statistic_counter_gc () 405 LANGUAGE plpgsql 406 AS $$ 407 DECLARE 408 my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; 409 my_rec RECORD; 410 my_sum RECORD; 411 my_meta INT8; 412 my_ranges INT8[]; 413 my_precisions INT8[]; 414 my_precision INT4; 415 my_i INT4; 416 min_slot INT8; 417 max_slot INT8; 418 end_slot INT8; 419 my_total INT8; 420 BEGIN 421 -- Do combination work for all numeric statistic events 422 FOR my_rec IN 423 SELECT imeta_serial_id 424 ,ranges 425 ,precisions 426 ,slug 427 FROM merchant_statistic_interval_meta 428 LOOP 429 PERFORM FROM merchant_statistic_interval_number_get (my_rec.slug); 430 431 my_meta = my_rec.imeta_serial_id; 432 my_ranges = my_rec.ranges; 433 my_precisions = my_rec.precisions; 434 435 FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) 436 LOOP 437 my_precision = my_precisions[my_i]; 438 IF 1 >= my_precision 439 THEN 440 -- Cannot coarsen in this case 441 CONTINUE; 442 END IF; 443 444 IF 1 = my_i 445 THEN 446 min_slot = 0; 447 ELSE 448 min_slot = my_ranges[my_i - 1]; 449 END IF; 450 end_slot = my_ranges[my_i]; 451 RAISE NOTICE 'Coarsening from [%,%) at %', my_time - end_slot, my_time - min_slot, my_precision; 452 453 LOOP 454 EXIT WHEN min_slot >= end_slot; 455 max_slot = min_slot + my_precision; 456 SELECT SUM(delta) AS total, 457 COUNT(*) AS matches, 458 MIN(nevent_serial_id) AS rep_serial_id 459 INTO my_sum 460 FROM merchant_statistic_counter_event 461 WHERE imeta_serial_id=my_meta 462 AND slot >= my_time - max_slot 463 AND slot < my_time - min_slot; 464 465 RAISE NOTICE 'Found % entries between [%,%)', my_sum.matches, my_time - max_slot, my_time - min_slot; 466 -- we only proceed if we had more then one match (optimization) 467 IF FOUND AND my_sum.matches > 1 468 THEN 469 my_total = my_sum.total; 470 471 RAISE NOTICE 'combining % entries to representative % for slots [%-%)', my_sum.matches, my_sum.rep_serial_id, my_time - max_slot, my_time - min_slot; 472 473 -- combine entries 474 DELETE FROM merchant_statistic_counter_event 475 WHERE imeta_serial_id=my_meta 476 AND slot >= my_time - max_slot 477 AND slot < my_time - min_slot 478 AND nevent_serial_id > my_sum.rep_serial_id; 479 -- Now update the representative to the sum 480 UPDATE merchant_statistic_counter_event SET 481 delta = my_total 482 WHERE imeta_serial_id = my_meta 483 AND nevent_serial_id = my_sum.rep_serial_id; 484 END IF; 485 min_slot = min_slot + my_precision; 486 END LOOP; -- min_slot to end_slot by precision loop 487 END LOOP; -- my_i loop 488 -- Finally, delete all events beyond the range we care about 489 490 RAISE NOTICE 'deleting entries of % before % - % = %', my_meta, my_time, my_ranges[array_length(my_ranges,1)], my_time - my_ranges[array_length(my_ranges,1)]; 491 DELETE FROM merchant_statistic_counter_event 492 WHERE imeta_serial_id=my_meta 493 AND slot < my_time - my_ranges[array_length(my_ranges,1)]; 494 END LOOP; -- my_rec loop 495 END $$; 496 COMMENT ON PROCEDURE merchant_statistic_counter_gc 497 IS 'Performs garbage collection and compaction of the merchant_statistic_counter_event table'; 498 499 500 501 DROP PROCEDURE IF EXISTS merchant_statistic_amount_gc; 502 CREATE OR REPLACE PROCEDURE merchant_statistic_amount_gc () 503 LANGUAGE plpgsql 504 AS $$ 505 DECLARE 506 my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; 507 my_rec RECORD; 508 my_sum RECORD; 509 my_meta INT8; 510 my_ranges INT8[]; 511 my_precisions INT8[]; 512 my_precision INT4; 513 my_currency TEXT; 514 my_i INT4; 515 min_slot INT8; 516 max_slot INT8; 517 end_slot INT8; 518 my_total_val INT8; 519 my_total_frac INT8; 520 BEGIN 521 -- Do combination work for all numeric statistic events 522 FOR my_rec IN 523 SELECT imeta_serial_id 524 ,ranges 525 ,precisions 526 ,slug 527 FROM merchant_statistic_interval_meta 528 LOOP 529 530 PERFORM FROM merchant_statistic_interval_amount_get (my_rec.slug); 531 532 my_meta = my_rec.imeta_serial_id; 533 my_ranges = my_rec.ranges; 534 my_precisions = my_rec.precisions; 535 FOR my_currency IN 536 SELECT DISTINCT delta_curr 537 FROM merchant_statistic_amount_event 538 WHERE imeta_serial_id = my_meta 539 LOOP 540 541 FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) 542 LOOP 543 my_precision = my_precisions[my_i]; 544 IF 1 >= my_precision 545 THEN 546 -- Cannot coarsen in this case 547 CONTINUE; 548 END IF; 549 550 IF 1 = my_i 551 THEN 552 min_slot = 0; 553 ELSE 554 min_slot = my_ranges[my_i - 1]; 555 END IF; 556 end_slot = my_ranges[my_i]; 557 558 RAISE NOTICE 'Coarsening from [%,%) at %', my_time - end_slot, my_time - min_slot, my_precision; 559 LOOP 560 EXIT WHEN min_slot >= end_slot; 561 max_slot = min_slot + my_precision; 562 SELECT SUM(delta_value) AS total_val, 563 SUM(delta_frac) AS total_frac, 564 COUNT(*) AS matches, 565 MIN(aevent_serial_id) AS rep_serial_id 566 INTO my_sum 567 FROM merchant_statistic_amount_event 568 WHERE imeta_serial_id=my_meta 569 AND delta_curr = my_currency 570 AND slot >= my_time - max_slot 571 AND slot < my_time - min_slot; 572 -- we only proceed if we had more then one match (optimization) 573 IF FOUND AND my_sum.matches > 1 574 THEN 575 -- normalize new total 576 my_total_frac = my_sum.total_frac % 100000000; 577 my_total_val = my_sum.total_val + my_sum.total_frac / 100000000; 578 579 -- combine entries 580 DELETE FROM merchant_statistic_amount_event 581 WHERE imeta_serial_id=my_meta 582 AND delta_curr = my_currency 583 AND slot >= my_time - max_slot 584 AND slot < my_time - min_slot 585 AND aevent_serial_id > my_sum.rep_serial_id; 586 -- Now update the representative to the sum 587 UPDATE merchant_statistic_amount_event SET 588 delta_value = my_total_val 589 ,delta_frac = my_total_frac 590 WHERE imeta_serial_id = my_meta 591 AND delta_curr = my_currency 592 AND aevent_serial_id = my_sum.rep_serial_id; 593 END IF; 594 min_slot = min_slot + my_precision; 595 END LOOP; -- min_slot to end_slot by precision loop 596 END LOOP; -- my_i loop 597 END LOOP; -- my_currency loop 598 -- Finally, delete all events beyond the range we care about 599 600 RAISE NOTICE 'deleting entries of % before % - % = %', my_meta, my_time, my_ranges[array_length(my_ranges,1)], my_time - my_ranges[array_length(my_ranges,1)]; 601 DELETE FROM merchant_statistic_amount_event 602 WHERE imeta_serial_id=my_meta 603 AND slot < my_time - my_ranges[array_length(my_ranges,1)]; 604 END LOOP; -- my_rec loop 605 END $$; 606 COMMENT ON PROCEDURE merchant_statistic_amount_gc 607 IS 'Performs garbage collection and compaction of the merchant_statistic_amount_event table'; 608 609 610 611 DROP PROCEDURE IF EXISTS merchant_statistic_bucket_gc; 612 CREATE OR REPLACE PROCEDURE merchant_statistic_bucket_gc () 613 LANGUAGE plpgsql 614 AS $$ 615 DECLARE 616 my_rec RECORD; 617 my_range TEXT; 618 my_now INT8; 619 my_end INT8; 620 BEGIN 621 my_now = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(0)::TIMESTAMP); -- seconds since epoch 622 FOR my_rec IN 623 SELECT bmeta_serial_id 624 ,stype 625 ,ranges[array_length(ranges,1)] AS range 626 ,ages[array_length(ages,1)] AS age 627 FROM merchant_statistic_bucket_meta 628 LOOP 629 my_range = '1 ' || my_rec.range::TEXT; 630 my_end = my_now - my_rec.age * EXTRACT(SECONDS FROM (SELECT my_range::INTERVAL)); -- age is given in multiples of the range (in seconds) 631 IF my_rec.stype = 'amount' 632 THEN 633 DELETE 634 FROM merchant_statistic_bucket_amount 635 WHERE bmeta_serial_id = my_rec.bmeta_serial_id 636 AND bucket_start < my_end; 637 ELSE 638 DELETE 639 FROM merchant_statistic_bucket_counter 640 WHERE bmeta_serial_id = my_rec.bmeta_serial_id 641 AND bucket_start < my_end; 642 END IF; 643 END LOOP; 644 END $$; 645 COMMENT ON PROCEDURE merchant_statistic_bucket_gc 646 IS 'Performs garbage collection of the merchant_statistic_bucket_counter and merchant_statistic_bucket_amount tables'; 647 648 649 650 -- The date_trunc may not be necessary if we assume it is already truncated 651 DROP FUNCTION IF EXISTS merchant_statistics_bucket_end; 652 CREATE FUNCTION merchant_statistics_bucket_end ( 653 IN in_bucket_start INT8, 654 IN in_range merchant.statistic_range, 655 OUT out_bucket_end INT8 656 ) 657 LANGUAGE plpgsql 658 AS $$ 659 BEGIN 660 IF in_range='quarter' 661 THEN 662 out_bucket_end = EXTRACT(EPOCH FROM CAST(date_trunc('quarter', to_timestamp(in_bucket_start)::date) + interval '3 months' AS date)); 663 ELSE 664 out_bucket_end = EXTRACT(EPOCH FROM CAST(to_timestamp(in_bucket_start)::date + ('1 ' || in_range)::interval AS date)); 665 END IF; 666 END $$; 667 COMMENT ON FUNCTION merchant_statistics_bucket_end 668 IS 'computes the end time of the bucket for an event at the current time given the desired bucket range';