fix: keep migration and blocker SQL postgres-safe
This commit is contained in:
@@ -4,7 +4,7 @@ from alembic import op
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
revision = "0018_blocker_installation_identity"
|
revision = "0018_blocker_installation"
|
||||||
down_revision = "0017_control_plane_foundation"
|
down_revision = "0017_control_plane_foundation"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ def blocker_filter_summary(session, *, tenant_id=None, agent_id=None, product_co
|
|||||||
query = _blockers_query(session, tenant_id=tenant_id, agent_id=agent_id, product_code=product_code, blocker_type=blocker_type, field=field, status=status, client=client, installation=installation, search=search, age=age)
|
query = _blockers_query(session, tenant_id=tenant_id, agent_id=agent_id, product_code=product_code, blocker_type=blocker_type, field=field, status=status, client=client, installation=installation, search=search, age=age)
|
||||||
open_count = func.sum(case((BlockedProduct.resolved_at.is_(None), 1), else_=0))
|
open_count = func.sum(case((BlockedProduct.resolved_at.is_(None), 1), else_=0))
|
||||||
open_count, = session.execute(query.with_only_columns(open_count).order_by(None)).one()
|
open_count, = session.execute(query.with_only_columns(open_count).order_by(None)).one()
|
||||||
rows = session.execute(query.with_only_columns(func.coalesce(BlockedProduct.field, "OTHER"), func.count(BlockedProduct.id)).group_by(func.coalesce(BlockedProduct.field, "OTHER")).order_by(None)).all()
|
rows = session.execute(_blocker_breakdown_query(query)).all()
|
||||||
breakdown = {field: 0 for field in BLOCKER_FIELDS}
|
breakdown = {field: 0 for field in BLOCKER_FIELDS}
|
||||||
for field_name, count in rows:
|
for field_name, count in rows:
|
||||||
key = str(field_name or "").upper()
|
key = str(field_name or "").upper()
|
||||||
@@ -279,6 +279,11 @@ def blocker_filter_summary(session, *, tenant_id=None, agent_id=None, product_co
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _blocker_breakdown_query(query):
|
||||||
|
"""Group on the source column itself for PostgreSQL-safe SQL semantics."""
|
||||||
|
return query.with_only_columns(BlockedProduct.field, func.count(BlockedProduct.id)).group_by(BlockedProduct.field).order_by(None)
|
||||||
|
|
||||||
|
|
||||||
def list_blockers(session, *, tenant_id=None, agent_id=None, product_code=None, page=1, page_size=50, blocker_type=None, field=None, resolved=False, client=None, installation=None, status="OPEN", age=None, search=None):
|
def list_blockers(session, *, tenant_id=None, agent_id=None, product_code=None, page=1, page_size=50, blocker_type=None, field=None, resolved=False, client=None, installation=None, status="OPEN", age=None, search=None):
|
||||||
page, page_size = _page(page, page_size)
|
page, page_size = _page(page, page_size)
|
||||||
if resolved and status == "OPEN":
|
if resolved and status == "OPEN":
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_alembic_revision_ids_fit_version_num_column():
|
||||||
|
versions = Path(__file__).parents[1] / "alembic" / "versions"
|
||||||
|
too_long = []
|
||||||
|
for path in versions.glob("*.py"):
|
||||||
|
spec = importlib.util.spec_from_file_location(f"migration_{path.stem}", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
revision = getattr(module, "revision", None)
|
||||||
|
if revision is not None and len(revision) > 32:
|
||||||
|
too_long.append((path.name, revision, len(revision)))
|
||||||
|
assert too_long == []
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
from central.app.audit import record_audit
|
from central.app.audit import record_audit
|
||||||
from central.app.blocked_products import upsert_blocker
|
from central.app.blocked_products import upsert_blocker
|
||||||
from central.app.control_plane import dashboard_summary, list_blockers, list_installations, list_jobs
|
from central.app.control_plane import _blocker_breakdown_query, dashboard_summary, list_blockers, list_installations, list_jobs
|
||||||
from central.app.events import record_event
|
from central.app.events import record_event
|
||||||
from central.app.models import Agent, AgentSnapshot, AuditEvent, BlockedProduct, Installation, Job, Tenant
|
from central.app.models import Agent, AgentSnapshot, AuditEvent, BlockedProduct, Installation, Job, Tenant
|
||||||
from central.tests.test_control_plane import setup_db
|
from central.tests.test_control_plane import setup_db
|
||||||
@@ -310,3 +311,13 @@ def test_lowercase_required_blocker_field_reaches_attention_candidates():
|
|||||||
attention = dashboard_summary(session, now=NOW)["attention_items"]
|
attention = dashboard_summary(session, now=NOW)["attention_items"]
|
||||||
|
|
||||||
assert any(item["link"] == f"/blockers/{blocker.id}" for item in attention)
|
assert any(item["link"] == f"/blockers/{blocker.id}" for item in attention)
|
||||||
|
|
||||||
|
|
||||||
|
def test_blocker_breakdown_query_groups_on_same_postgres_expression():
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
query = select(BlockedProduct, Tenant).join(Tenant, Tenant.id == BlockedProduct.tenant_id)
|
||||||
|
sql = str(_blocker_breakdown_query(query).compile(dialect=postgresql.dialect()))
|
||||||
|
|
||||||
|
assert "GROUP BY blocked_products.field" in sql
|
||||||
|
assert "GROUP BY coalesce" not in sql
|
||||||
|
|||||||
Reference in New Issue
Block a user