-- test/sql/20_maintenance.sql -- Tests for maintenance module: register_verify, register_rebuild_totals, register_rebuild_cache, register_stats BEGIN; SELECT plan(35); -- ============================================================ -- Setup: create a balance register with data -- ============================================================ SELECT accum.register_create( name := 'maint', dimensions := '{"warehouse": "int", "product": "int"}', resources := '{"quantity": "numeric(18,4)", "amount": "numeric(18,2)"}', kind := 'balance' ); -- Post test data SELECT accum.register_post('maint', '[ {"recorder":"doc:1","period":"2026-01-15","warehouse":1,"product":100,"quantity":50,"amount":500}, {"recorder":"doc:2","period":"2026-01-20","warehouse":1,"product":101,"quantity":30,"amount":300}, {"recorder":"doc:3","period":"2026-02-10","warehouse":2,"product":100,"quantity":20,"amount":200}, {"recorder":"doc:4","period":"2026-03-05","warehouse":1,"product":100,"quantity":10,"amount":100} ]'); -- ============================================================ -- TEST 1: verify on consistent register → all OK -- ============================================================ SELECT is( (SELECT count(*) FROM accum.register_verify('maint') WHERE status != 'OK'), 0::bigint, 'verify on consistent register should return all OK' ); -- ============================================================ -- TEST 2: verify returns balance_cache checks -- ============================================================ SELECT ok( (SELECT count(*) > 0 FROM accum.register_verify('maint') WHERE check_type = 'balance_cache'), 'verify should include balance_cache checks' ); -- ============================================================ -- TEST 3: verify returns totals_month checks -- ============================================================ SELECT ok( (SELECT count(*) > 0 FROM accum.register_verify('maint') WHERE check_type = 'totals_month'), 'verify should include totals_month checks' ); -- ============================================================ -- TEST 4: verify returns totals_year checks -- ============================================================ SELECT ok( (SELECT count(*) > 0 FROM accum.register_verify('maint') WHERE check_type = 'totals_year'), 'verify should include totals_year checks' ); -- ============================================================ -- TEST 5: Introduce MISMATCH in balance_cache → verify detects it -- ============================================================ SET pg_accumulator.allow_internal = 'on'; UPDATE accum.maint_balance_cache SET quantity = quantity + 999 WHERE dim_hash = (SELECT dim_hash FROM accum.maint_balance_cache LIMIT 1); RESET pg_accumulator.allow_internal; SELECT ok( (SELECT count(*) > 0 FROM accum.register_verify('maint') WHERE check_type = 'balance_cache' AND status = 'MISMATCH'), 'verify should detect MISMATCH after corrupting balance_cache' ); -- Restore for further tests SET pg_accumulator.allow_internal = 'on'; UPDATE accum.maint_balance_cache bc SET quantity = agg.quantity FROM ( SELECT dim_hash, SUM(quantity) AS quantity FROM accum.maint_movements GROUP BY dim_hash ) agg WHERE bc.dim_hash = agg.dim_hash; RESET pg_accumulator.allow_internal; -- ============================================================ -- TEST 6: Introduce MISMATCH in totals_month → verify detects it -- ============================================================ SET pg_accumulator.allow_internal = 'on'; UPDATE accum.maint_totals_month SET quantity = quantity + 777 WHERE dim_hash = (SELECT dim_hash FROM accum.maint_totals_month LIMIT 1); RESET pg_accumulator.allow_internal; SELECT ok( (SELECT count(*) > 0 FROM accum.register_verify('maint') WHERE check_type = 'totals_month' AND status = 'MISMATCH'), 'verify should detect MISMATCH in totals_month' ); -- ============================================================ -- TEST 7: register_rebuild_totals → totals recalculated correctly -- ============================================================ SELECT ok( accum.register_rebuild_totals('maint') > 0, 'rebuild_totals should return positive count' ); -- ============================================================ -- TEST 8: After rebuild_totals → verify all OK -- ============================================================ SELECT is( (SELECT count(*) FROM accum.register_verify('maint') WHERE check_type IN ('totals_month', 'totals_year') AND status != 'OK'), 0::bigint, 'After rebuild_totals, all totals checks should be OK' ); -- ============================================================ -- TEST 9: register_rebuild_cache (full) → cache recalculated -- ============================================================ -- First corrupt cache SET pg_accumulator.allow_internal = 'on'; UPDATE accum.maint_balance_cache SET quantity = 0, amount = 0; RESET pg_accumulator.allow_internal; SELECT ok( accum.register_rebuild_cache('maint') > 0, 'rebuild_cache should return positive count' ); -- ============================================================ -- TEST 10: After rebuild_cache → verify cache OK -- ============================================================ SELECT is( (SELECT count(*) FROM accum.register_verify('maint') WHERE check_type = 'balance_cache' AND status != 'OK'), 0::bigint, 'After rebuild_cache, balance_cache checks should be OK' ); -- ============================================================ -- TEST 11: rebuild_cache partial (specific dim_hash) -- ============================================================ -- Get a dim_hash and corrupt it DO $$ DECLARE v_hash bigint; BEGIN PERFORM set_config('pg_accumulator.allow_internal', 'on', true); SELECT dim_hash INTO v_hash FROM accum.maint_balance_cache LIMIT 1; UPDATE accum.maint_balance_cache SET quantity = -9999 WHERE dim_hash = v_hash; END; $$; -- Rebuild only that dim_hash SELECT ok( accum.register_rebuild_cache('maint', (SELECT dim_hash FROM accum.maint_balance_cache WHERE quantity = -9999)) = 1, 'Partial rebuild_cache should rebuild exactly 1 row' ); -- ============================================================ -- TEST 12: After partial rebuild → verify all OK -- ============================================================ SELECT is( (SELECT count(*) FROM accum.register_verify('maint') WHERE check_type = 'balance_cache' AND status != 'OK'), 0::bigint, 'After partial rebuild_cache, all balance_cache checks should be OK' ); -- ============================================================ -- TEST 13: register_stats returns valid structure -- ============================================================ SELECT ok( accum.register_stats('maint') ? 'movements_count', 'stats should contain movements_count' ); SELECT ok( accum.register_stats('maint') ? 'partitions_count', 'stats should contain partitions_count' ); SELECT ok( accum.register_stats('maint') ? 'cache_rows', 'stats should contain cache_rows' ); SELECT ok( accum.register_stats('maint') ? 'totals_month_rows', 'stats should contain totals_month_rows' ); SELECT ok( accum.register_stats('maint') ? 'totals_year_rows', 'stats should contain totals_year_rows' ); SELECT ok( accum.register_stats('maint') ? 'table_sizes', 'stats should contain table_sizes' ); -- ============================================================ -- TEST 14: stats returns correct movement count -- ============================================================ SELECT is( (accum.register_stats('maint')->>'movements_count')::int, 4, 'stats should report 4 movements' ); -- ============================================================ -- TEST 15: stats returns correct cache row count -- ============================================================ SELECT is( (accum.register_stats('maint')->>'cache_rows')::int, (SELECT count(*)::int FROM accum.maint_balance_cache), 'stats cache_rows should match actual count' ); -- ============================================================ -- TEST 16: verify + rebuild circle: verify MISMATCH → rebuild → verify OK -- ============================================================ -- Corrupt both totals and cache SET pg_accumulator.allow_internal = 'on'; UPDATE accum.maint_balance_cache SET quantity = -1111; UPDATE accum.maint_totals_month SET quantity = -2222; RESET pg_accumulator.allow_internal; -- Verify detects issues SELECT ok( (SELECT count(*) > 0 FROM accum.register_verify('maint') WHERE status != 'OK'), 'verify should detect issues after corruption' ); -- Rebuild totals first, then cache SELECT accum.register_rebuild_totals('maint'); SELECT accum.register_rebuild_cache('maint'); -- Verify all OK now SELECT is( (SELECT count(*) FROM accum.register_verify('maint') WHERE status != 'OK'), 0::bigint, 'After full rebuild cycle, verify should report all OK' ); -- ============================================================ -- TEST 17: rebuild on empty register -- ============================================================ SELECT accum.register_create( name := 'maint_empty', dimensions := '{"x": "int"}', resources := '{"val": "numeric"}', kind := 'balance' ); SELECT is( accum.register_rebuild_totals('maint_empty'), 0, 'rebuild_totals on empty register should return 0' ); SELECT is( accum.register_rebuild_cache('maint_empty'), 0, 'rebuild_cache on empty register should return 0' ); -- ============================================================ -- TEST 18: verify on empty register → all OK (no rows) -- ============================================================ SELECT is( (SELECT count(*) FROM accum.register_verify('maint_empty') WHERE status != 'OK'), 0::bigint, 'verify on empty register should have no mismatches' ); -- ============================================================ -- TEST 19: stats on empty register → zeros -- ============================================================ SELECT is( (accum.register_stats('maint_empty')->>'movements_count')::int, 0, 'stats on empty register should report 0 movements' ); -- ============================================================ -- TEST 20: rebuild_cache fails on turnover register -- ============================================================ SELECT accum.register_create( name := 'maint_turnover', dimensions := '{"x": "int"}', resources := '{"val": "numeric"}', kind := 'turnover' ); SELECT throws_ok( $$SELECT accum.register_rebuild_cache('maint_turnover')$$, NULL, NULL, 'rebuild_cache should fail on turnover register' ); -- ============================================================ -- TEST 21: verify on nonexistent register → error -- ============================================================ SELECT throws_ok( $$SELECT * FROM accum.register_verify('nonexistent')$$, NULL, NULL, 'verify should fail on nonexistent register' ); -- ============================================================ -- TEST 22: verify returns totals_day checks -- ============================================================ SELECT ok( (SELECT count(*) > 0 FROM accum.register_verify('maint') WHERE check_type = 'totals_day'), 'verify should include totals_day checks' ); -- ============================================================ -- TEST 23: Introduce MISMATCH in totals_day → verify detects it -- ============================================================ SET pg_accumulator.allow_internal = 'on'; UPDATE accum.maint_totals_day SET quantity = quantity + 555 WHERE dim_hash = (SELECT dim_hash FROM accum.maint_totals_day LIMIT 1); RESET pg_accumulator.allow_internal; SELECT ok( (SELECT count(*) > 0 FROM accum.register_verify('maint') WHERE check_type = 'totals_day' AND status = 'MISMATCH'), 'verify should detect MISMATCH in totals_day' ); -- Rebuild to restore consistency SELECT accum.register_rebuild_totals('maint'); -- ============================================================ -- TEST 24: stats includes totals_day_rows -- ============================================================ SELECT ok( accum.register_stats('maint') ? 'totals_day_rows', 'stats should contain totals_day_rows' ); -- ============================================================ -- TEST 25: protection trigger blocks direct UPDATE on movements -- ============================================================ SELECT throws_ok( $$UPDATE accum.maint_movements SET quantity = 0 WHERE id = (SELECT id FROM accum.maint_movements LIMIT 1)$$, NULL, 'Direct UPDATE on movements is not allowed. Use register_repost() instead.', 'Direct UPDATE on movements should be blocked' ); -- ============================================================ -- TEST 26: protection trigger blocks direct INSERT on totals_month -- ============================================================ SELECT throws_ok( $$INSERT INTO accum.maint_totals_month (dim_hash, period, warehouse, product, quantity, amount) VALUES (0, '2026-01-01', 1, 1, 0, 0)$$, NULL, 'Direct modification of derived table is not allowed. Use register_rebuild_totals() / register_rebuild_cache() for corrections.', 'Direct INSERT on totals_month should be blocked' ); -- ============================================================ -- TEST 27: protection trigger blocks direct INSERT on totals_day -- ============================================================ SELECT throws_ok( $$INSERT INTO accum.maint_totals_day (dim_hash, period, warehouse, product, quantity, amount) VALUES (0, '2026-01-01', 1, 1, 0, 0)$$, NULL, 'Direct modification of derived table is not allowed. Use register_rebuild_totals() / register_rebuild_cache() for corrections.', 'Direct INSERT on totals_day should be blocked' ); -- ============================================================ -- TEST 28: GUC bypass allows direct modification when set -- ============================================================ SET pg_accumulator.allow_internal = 'on'; SELECT lives_ok( $$UPDATE accum.maint_balance_cache SET quantity = quantity WHERE dim_hash = (SELECT dim_hash FROM accum.maint_balance_cache LIMIT 1)$$, 'Direct UPDATE should be allowed when pg_accumulator.allow_internal = on' ); RESET pg_accumulator.allow_internal; -- ============================================================ -- Cleanup -- ============================================================ SELECT accum.register_drop('maint', true); SELECT accum.register_drop('maint_empty', true); SELECT accum.register_drop('maint_turnover', true); SELECT * FROM finish(); ROLLBACK;