"""SQL allow-list and Cube query caps. The Phase 8 gate demands zero SQL executed outside the allow-list. That is a property of this module, so it is tested here rather than inferred from the eval run. """ import pytest from guardrails import GuardrailViolation, check_cube_query, check_sql MAX_ROWS = 5000 def test_plain_select_is_allowed_and_capped(): out = check_sql("SELECT tag_id FROM tags", max_rows=MAX_ROWS) assert "LIMIT 5000" in out.upper() def test_smaller_limit_is_honoured(): out = check_sql("SELECT tag_id FROM tags LIMIT 10", max_rows=MAX_ROWS) assert "LIMIT 10" in out.upper() def test_larger_limit_is_capped(): out = check_sql("SELECT tag_id FROM tags LIMIT 999999", max_rows=MAX_ROWS) assert "LIMIT 5000" in out.upper() @pytest.mark.parametrize( "sql", [ "INSERT INTO equipment VALUES ('X')", "UPDATE tags SET display_name = 'x'", "DELETE FROM doc_chunks", "DROP TABLE tags", "CREATE TABLE t (i int)", ], ) def test_writes_are_refused(sql): with pytest.raises(GuardrailViolation): check_sql(sql, max_rows=MAX_ROWS) def test_stacked_statements_are_refused(): with pytest.raises(GuardrailViolation) as caught: check_sql("SELECT 1 FROM tags; DROP TABLE tags", max_rows=MAX_ROWS) assert caught.value.rule in {"multiple_statements", "write_operation"} def test_table_outside_the_allow_list_is_refused(): with pytest.raises(GuardrailViolation) as caught: check_sql("SELECT * FROM pg_shadow", max_rows=MAX_ROWS) assert caught.value.rule == "table_not_allowed" def test_subquery_tables_are_checked_too(): with pytest.raises(GuardrailViolation): check_sql( "SELECT * FROM tags WHERE tag_id IN (SELECT usename FROM pg_shadow)", max_rows=MAX_ROWS, ) def test_cube_query_needs_a_pinned_window(): with pytest.raises(GuardrailViolation) as caught: check_cube_query({"measures": ["alarm_activity.alarm_count"]}, max_rows=MAX_ROWS) assert caught.value.rule == "unpinned_time_window" def test_cube_query_needs_a_date_range(): with pytest.raises(GuardrailViolation): check_cube_query( {"timeDimensions": [{"dimension": "alarm_activity.event_time"}]}, max_rows=MAX_ROWS, ) def test_cube_query_is_capped(): out = check_cube_query( { "timeDimensions": [ {"dimension": "alarm_activity.event_time", "dateRange": ["2026-08-13", "2026-08-20"]} ], "limit": 1_000_000, }, max_rows=MAX_ROWS, ) assert out["limit"] == MAX_ROWS def test_cube_query_gets_the_site_timezone_pinned(): """A Cube query without a timezone runs in UTC while the answer says AEST. rolling_window() produces boundary strings in site local time. Cube parses a query with no timezone as UTC, so the window queried was shifted by the site's UTC offset - ten hours - and nothing in the answer said so. Found by reading the 'show working' panel in a browser, not by any test. """ query = { "measures": ["alarms.alarm_count"], "timeDimensions": [ { "dimension": "alarms.event_time", "dateRange": ["2026-08-14T15:20:16", "2026-08-21T15:20:16"], } ], } capped = check_cube_query(query, max_rows=5000, site_timezone="Australia/Sydney") assert capped["timezone"] == "Australia/Sydney" def test_an_explicit_timezone_on_the_query_is_not_overridden(): query = { "measures": ["alarms.alarm_count"], "timeDimensions": [ {"dimension": "alarms.event_time", "dateRange": ["2026-08-14", "2026-08-21"]} ], "timezone": "UTC", } capped = check_cube_query(query, max_rows=5000, site_timezone="Australia/Sydney") assert capped["timezone"] == "UTC"