From 2c8df1433dd1858ecfa34f6188ac598ad6d193c0 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Tue, 10 Mar 2026 13:56:38 +0100 Subject: [PATCH] Fix SQLAlchemy 2.x compatibility: wrap raw SQL in text() SQLAlchemy 2.x requires raw SQL strings to be explicitly wrapped in text(). Fixed 16 instances across 5 route files. Co-Authored-By: Claude Opus 4.6 --- .../compliance/api/company_profile_routes.py | 25 ++++++++++--------- .../compliance/api/compliance_scope_routes.py | 13 +++++----- .../compliance/api/generation_routes.py | 2 +- .../compliance/api/import_routes.py | 19 +++++++------- .../compliance/api/screening_routes.py | 21 ++++++++-------- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/backend-compliance/compliance/api/company_profile_routes.py b/backend-compliance/compliance/api/company_profile_routes.py index 0b44a86..d8c2740 100644 --- a/backend-compliance/compliance/api/company_profile_routes.py +++ b/backend-compliance/compliance/api/company_profile_routes.py @@ -15,6 +15,7 @@ from typing import Optional from fastapi import APIRouter, HTTPException, Header from pydantic import BaseModel +from sqlalchemy import text from database import SessionLocal @@ -224,9 +225,9 @@ def log_audit(db, tenant_id: str, action: str, changed_fields: Optional[dict], c """Write an audit log entry.""" try: db.execute( - """INSERT INTO compliance_company_profile_audit + text("""INSERT INTO compliance_company_profile_audit (tenant_id, action, changed_fields, changed_by) - VALUES (:tenant_id, :action, :fields::jsonb, :changed_by)""", + VALUES (:tenant_id, :action, :fields::jsonb, :changed_by)"""), { "tenant_id": tenant_id, "action": action, @@ -252,7 +253,7 @@ async def get_company_profile( db = SessionLocal() try: result = db.execute( - f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tenant_id", + text(f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tenant_id"), {"tenant_id": tid}, ) row = result.fetchone() @@ -276,7 +277,7 @@ async def upsert_company_profile( try: # Check if profile exists existing = db.execute( - "SELECT id FROM compliance_company_profiles WHERE tenant_id = :tid", + text("SELECT id FROM compliance_company_profiles WHERE tenant_id = :tid"), {"tid": tid}, ).fetchone() @@ -285,7 +286,7 @@ async def upsert_company_profile( completed_at_clause = ", completed_at = NOW()" if profile.is_complete else ", completed_at = NULL" db.execute( - f"""INSERT INTO compliance_company_profiles + text(f"""INSERT INTO compliance_company_profiles (tenant_id, company_name, legal_form, industry, founded_year, business_model, offerings, company_size, employee_count, annual_revenue, headquarters_country, headquarters_city, has_international_locations, @@ -344,7 +345,7 @@ async def upsert_company_profile( supervisory_authority = EXCLUDED.supervisory_authority, review_cycle_months = EXCLUDED.review_cycle_months, updated_at = NOW() - {completed_at_clause}""", + {completed_at_clause}"""), { "tid": tid, "company_name": profile.company_name, @@ -392,7 +393,7 @@ async def upsert_company_profile( # Fetch and return result = db.execute( - f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid", + text(f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid"), {"tid": tid}, ) row = result.fetchone() @@ -415,7 +416,7 @@ async def delete_company_profile( db = SessionLocal() try: existing = db.execute( - "SELECT id FROM compliance_company_profiles WHERE tenant_id = :tid", + text("SELECT id FROM compliance_company_profiles WHERE tenant_id = :tid"), {"tid": tid}, ).fetchone() @@ -423,7 +424,7 @@ async def delete_company_profile( raise HTTPException(status_code=404, detail="Company profile not found") db.execute( - "DELETE FROM compliance_company_profiles WHERE tenant_id = :tid", + text("DELETE FROM compliance_company_profiles WHERE tenant_id = :tid"), {"tid": tid}, ) @@ -451,7 +452,7 @@ async def get_template_context( db = SessionLocal() try: result = db.execute( - f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid", + text(f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid"), {"tid": tid}, ) row = result.fetchone() @@ -513,11 +514,11 @@ async def get_audit_log( db = SessionLocal() try: result = db.execute( - """SELECT id, action, changed_fields, changed_by, created_at + text("""SELECT id, action, changed_fields, changed_by, created_at FROM compliance_company_profile_audit WHERE tenant_id = :tid ORDER BY created_at DESC - LIMIT 100""", + LIMIT 100"""), {"tid": tid}, ) rows = result.fetchall() diff --git a/backend-compliance/compliance/api/compliance_scope_routes.py b/backend-compliance/compliance/api/compliance_scope_routes.py index 408ed3d..1b94173 100644 --- a/backend-compliance/compliance/api/compliance_scope_routes.py +++ b/backend-compliance/compliance/api/compliance_scope_routes.py @@ -15,6 +15,7 @@ from typing import Any, Optional from fastapi import APIRouter, HTTPException, Header from pydantic import BaseModel +from sqlalchemy import text from database import SessionLocal @@ -75,13 +76,13 @@ async def get_compliance_scope( db = SessionLocal() try: row = db.execute( - """SELECT tenant_id, + text("""SELECT tenant_id, state->'compliance_scope' AS scope, created_at, updated_at FROM sdk_states WHERE tenant_id = :tid - AND state ? 'compliance_scope'""", + AND state ? 'compliance_scope'"""), {"tid": tid}, ).fetchone() @@ -106,22 +107,22 @@ async def upsert_compliance_scope( db = SessionLocal() try: db.execute( - """INSERT INTO sdk_states (tenant_id, state) + text("""INSERT INTO sdk_states (tenant_id, state) VALUES (:tid, jsonb_build_object('compliance_scope', :scope::jsonb)) ON CONFLICT (tenant_id) DO UPDATE SET state = sdk_states.state || jsonb_build_object('compliance_scope', :scope::jsonb), - updated_at = NOW()""", + updated_at = NOW()"""), {"tid": tid, "scope": scope_json}, ) db.commit() row = db.execute( - """SELECT tenant_id, + text("""SELECT tenant_id, state->'compliance_scope' AS scope, created_at, updated_at FROM sdk_states - WHERE tenant_id = :tid""", + WHERE tenant_id = :tid"""), {"tid": tid}, ).fetchone() diff --git a/backend-compliance/compliance/api/generation_routes.py b/backend-compliance/compliance/api/generation_routes.py index a8b787a..286bd88 100644 --- a/backend-compliance/compliance/api/generation_routes.py +++ b/backend-compliance/compliance/api/generation_routes.py @@ -39,7 +39,7 @@ def _get_template_context(db, tid: str) -> dict: cp_db = SessionLocal() try: result = cp_db.execute( - f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid", + text(f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid"), {"tid": tid}, ) row = result.fetchone() diff --git a/backend-compliance/compliance/api/import_routes.py b/backend-compliance/compliance/api/import_routes.py index 14d8044..25ab62d 100644 --- a/backend-compliance/compliance/api/import_routes.py +++ b/backend-compliance/compliance/api/import_routes.py @@ -15,6 +15,7 @@ from typing import Optional import httpx from fastapi import APIRouter, File, Form, Header, UploadFile, HTTPException from pydantic import BaseModel +from sqlalchemy import text from database import SessionLocal @@ -291,11 +292,11 @@ async def analyze_document( db = SessionLocal() try: db.execute( - """INSERT INTO compliance_imported_documents + text("""INSERT INTO compliance_imported_documents (id, tenant_id, filename, file_type, file_size, detected_type, detection_confidence, extracted_text, extracted_entities, recommendations, status, analyzed_at) VALUES (:id, :tenant_id, :filename, :file_type, :file_size, :detected_type, :confidence, - :text, :entities::jsonb, :recommendations::jsonb, 'analyzed', NOW())""", + :text, :entities::jsonb, :recommendations::jsonb, 'analyzed', NOW())"""), { "id": doc_id, "tenant_id": tenant_id, @@ -313,9 +314,9 @@ async def analyze_document( if total_gaps > 0: import json db.execute( - """INSERT INTO compliance_gap_analyses + text("""INSERT INTO compliance_gap_analyses (tenant_id, document_id, total_gaps, critical_gaps, high_gaps, medium_gaps, low_gaps, gaps, recommended_packages) - VALUES (:tenant_id, :document_id, :total, :critical, :high, :medium, :low, :gaps::jsonb, :packages::jsonb)""", + VALUES (:tenant_id, :document_id, :total, :critical, :high, :medium, :low, :gaps::jsonb, :packages::jsonb)"""), { "tenant_id": tenant_id, "document_id": doc_id, @@ -358,7 +359,7 @@ async def get_gap_analysis( db = SessionLocal() try: result = db.execute( - "SELECT * FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid", + text("SELECT * FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid"), {"doc_id": document_id, "tid": tid}, ).fetchone() if not result: @@ -374,11 +375,11 @@ async def list_documents(tenant_id: str = "default"): db = SessionLocal() try: result = db.execute( - """SELECT id, filename, file_type, file_size, detected_type, detection_confidence, + text("""SELECT id, filename, file_type, file_size, detected_type, detection_confidence, extracted_entities, recommendations, status, analyzed_at, created_at FROM compliance_imported_documents WHERE tenant_id = :tenant_id - ORDER BY created_at DESC""", + ORDER BY created_at DESC"""), {"tenant_id": tenant_id}, ) rows = result.fetchall() @@ -424,11 +425,11 @@ async def delete_document( try: # Delete gap analysis first (FK dependency) db.execute( - "DELETE FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid", + text("DELETE FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid"), {"doc_id": document_id, "tid": tid}, ) result = db.execute( - "DELETE FROM compliance_imported_documents WHERE id = :doc_id AND tenant_id = :tid", + text("DELETE FROM compliance_imported_documents WHERE id = :doc_id AND tenant_id = :tid"), {"doc_id": document_id, "tid": tid}, ) db.commit() diff --git a/backend-compliance/compliance/api/screening_routes.py b/backend-compliance/compliance/api/screening_routes.py index 307ca67..9b9ee16 100644 --- a/backend-compliance/compliance/api/screening_routes.py +++ b/backend-compliance/compliance/api/screening_routes.py @@ -17,6 +17,7 @@ from typing import Optional import httpx from fastapi import APIRouter, File, Form, UploadFile, HTTPException from pydantic import BaseModel +from sqlalchemy import text from database import SessionLocal @@ -366,13 +367,13 @@ async def scan_dependencies( db = SessionLocal() try: db.execute( - """INSERT INTO compliance_screenings + text("""INSERT INTO compliance_screenings (id, tenant_id, status, sbom_format, sbom_version, total_components, total_issues, critical_issues, high_issues, medium_issues, low_issues, sbom_data, started_at, completed_at) VALUES (:id, :tenant_id, 'completed', 'CycloneDX', '1.5', :total_components, :total_issues, :critical, :high, :medium, :low, - :sbom_data::jsonb, :started_at, :completed_at)""", + :sbom_data::jsonb, :started_at, :completed_at)"""), { "id": screening_id, "tenant_id": tenant_id, @@ -391,11 +392,11 @@ async def scan_dependencies( # Persist security issues for issue in issues: db.execute( - """INSERT INTO compliance_security_issues + text("""INSERT INTO compliance_security_issues (id, screening_id, severity, title, description, cve, cvss, affected_component, affected_version, fixed_in, remediation, status) VALUES (:id, :screening_id, :severity, :title, :description, :cve, :cvss, - :component, :version, :fixed_in, :remediation, :status)""", + :component, :version, :fixed_in, :remediation, :status)"""), { "id": issue["id"], "screening_id": screening_id, @@ -486,10 +487,10 @@ async def get_screening(screening_id: str): db = SessionLocal() try: result = db.execute( - """SELECT id, status, sbom_format, sbom_version, + text("""SELECT id, status, sbom_format, sbom_version, total_components, total_issues, critical_issues, high_issues, medium_issues, low_issues, sbom_data, started_at, completed_at - FROM compliance_screenings WHERE id = :id""", + FROM compliance_screenings WHERE id = :id"""), {"id": screening_id}, ) row = result.fetchone() @@ -498,9 +499,9 @@ async def get_screening(screening_id: str): # Fetch issues issues_result = db.execute( - """SELECT id, severity, title, description, cve, cvss, + text("""SELECT id, severity, title, description, cve, cvss, affected_component, affected_version, fixed_in, remediation, status - FROM compliance_security_issues WHERE screening_id = :id""", + FROM compliance_security_issues WHERE screening_id = :id"""), {"id": screening_id}, ) issues_rows = issues_result.fetchall() @@ -566,12 +567,12 @@ async def list_screenings(tenant_id: str = "default"): db = SessionLocal() try: result = db.execute( - """SELECT id, status, total_components, total_issues, + text("""SELECT id, status, total_components, total_issues, critical_issues, high_issues, medium_issues, low_issues, started_at, completed_at, created_at FROM compliance_screenings WHERE tenant_id = :tenant_id - ORDER BY created_at DESC""", + ORDER BY created_at DESC"""), {"tenant_id": tenant_id}, ) rows = result.fetchall()