Skip to content

Commit 606a107

Browse files
committed
feat: Update environment configurations and enhance SQL handling with new validation tests
- Refactor environment files to include a new debug configuration. - Change log level in the main config to DEBUG for better traceability. - Modify SQL queries to use `literal_column("*")` for selecting all columns. - Introduce validation tests to ensure correct handling of NULL values and spec overrides.
1 parent 8ba4118 commit 606a107

11 files changed

Lines changed: 258 additions & 24 deletions

File tree

.env.integration.enc

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{
2-
"data": "ENC[AES256_GCM,data:03PdEnWcPyfmiC1srkK2y7nu8YNiRFzEpMEDxu9cSqq3mcM1b3YJTLhghGe0U846Z+6RPMjJM/Q3NCbel9IezAqpL/j9F59XRXICYjupu7QLxNbk0nXjusMMrtNDzDF4kdt+/jGN66JXpL/zQDSFxTKxMJyL2f5nyR9blzyBjdfmTpDzyH5QvEUm+6pt4EMn/JHLgtFhfTHcaYv7lDAx6nwSibLc8ejZIC/YlEjNdrhFJhq3NCnVgu7XrUknIFp/UkhyzA==,iv:KVXUKy2vfxQCnN4OCthnFK1IcyaKNxeviFmbeCdgxp4=,tag:VdgVfERKe2BosKGFjLU6RQ==,type:str]",
2+
"data": "ENC[AES256_GCM,data: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,iv:a5ZAZ8sWFNO26CxwbHnClux1Ha1HyZytj3DwUoDanjM=,tag:AeQKLWvJKkebCIqzMjkMwQ==,type:str]",
33
"sops": {
4-
"lastmodified": "2026-02-24T15:03:09Z",
5-
"mac": "ENC[AES256_GCM,data:w6EcTJyKSv46Kmi1WzzsqmRXmhhyPdP/I+xPGokylE3MlNOHxjl4n85eQ9A6w/DFLsp24nml40aNnMLnjILuJ+im0YLmavzVnGA1cazGsrGoc+1dRvEsW8wLDaVwyjT+jajSppjqjqj1g9xcshzn9z2W7SVqOWRZ+1WI7urIFBA=,iv:XG5kPiuCmovwdzDgGG5obmC+lxECnLuErm1vlumTHls=,tag:FjOK0PexarbGk6vxBmq8Cw==,type:str]",
4+
"lastmodified": "2026-03-18T16:08:54Z",
5+
"mac": "ENC[AES256_GCM,data:tZktiCSC2x52QXIKXGitU9WV77qAzK5AvV3J+AROhmpomyM28bXw2XJLKVYj/5kRraumHXRCJnsV1F4bwQiqeOJW0XUyUP1erwmD9kG8yO5ggISzXWmK2fuSnhSaI4EON0NazxtKk+i63zgE+aOY3vnAdqkycbvc1xkCvSx3MXM=,iv:X9M4nwRAnQA32F3CkAGHCxOqOxpSNOQAYoYBtV+Zsw4=,tag:5otZTGkbco783kpfjsKMPg==,type:str]",
66
"pgp": [
77
{
8-
"created_at": "2026-02-24T15:03:09Z",
9-
"enc": "-----BEGIN PGP MESSAGE-----\n\nhF4Df+t0WwSeCuMSAQdAGCQoRiOLBVwJoniMNhoC6IS0t4s972FxKj8DQ5bBZwIw\nxbtYu9Tpqh2BbSAUNdpSlpHN2srXV9H7sTDt7YGBWlAqyIuaLPTx+QjM7irGhhtW\n0l4BWnZBkUklIv7BnhM3lDMPrdZZW4OLOycC39eLdC6MwBps5G5Zi/eiF8CyVOS1\nbCsrE2oGocMIIOTD8p69RUmvKK7sMUavwOi1yP2+291esaQcF+Ws05jDhLhHVMzM\n=pWbn\n-----END PGP MESSAGE-----",
8+
"created_at": "2026-03-18T16:08:54Z",
9+
"enc": "-----BEGIN PGP MESSAGE-----\n\nhF4Df+t0WwSeCuMSAQdAH7Bp/SMJIvD51M1rgdp0Vg+K7ldmBq86cWQxwqBhdQcw\ngpcUYdzpvF5JB390ROkc1mUrrfl5L9CL6nGlCqCQ20MI5qBlX3Jvlr2HVcK/j5aL\n0l4B1wv2Gccag5vvBjX/9Q9XpVO2ejZRzka7L051oQw5aZMQOmqj/vi6Yhd0FtBu\nqJryo1KKOqZWzHvqZ0bYYja9oZfTIR7mCqI1df6/9fGPZdpdH8J+G72IwyRgmul2\n=wIls\n-----END PGP MESSAGE-----",
1010
"fp": "37D38A6C0248214B007B6C5685E825F3377228D6"
1111
},
1212
{
13-
"created_at": "2026-02-24T15:03:09Z",
14-
"enc": "-----BEGIN PGP MESSAGE-----\n\nhF4D5jdJleHfCY0SAQdAr1p7uwx335J1uIfBAcRtGmP4m8opDyqGk5tHdUzd3T4w\nBeyVBmD+O0sxJAxugZvPbN9wd0bVkn3FI5xteGM0KeN2urUPDhIYF16WVSDkpKHb\n0l4BPbJThh1hoMQPIj/+VialD6rWpKX7DvB/BOE/iYfkYdmZGhHUPLJTIKNBzYOF\nkp/e8fVxzH+hpUbQtqDyk6mksLfG/Bc+IJFqz518ZaL/NGA6sdHlIb6zq+0H74N0\n=ZjVZ\n-----END PGP MESSAGE-----",
13+
"created_at": "2026-03-18T16:08:54Z",
14+
"enc": "-----BEGIN PGP MESSAGE-----\n\nhF4D5jdJleHfCY0SAQdAbvs5ko+XMNfB++a0WZNX0+S1NE/BbeFO5P9v0FCaixgw\n5IkvY3GDkTlZcrNPpphIVzILBcY3LiVymlbAgL43/jZVoMjon89FIkpoQqV2wUT0\n0l4B3QSpNdDQb615oalHQkLL/gIkC0/gvefm1dK1Czl/aKmG55bVo8+8C7e3aMKw\nT39TTSvEI8/ZTZIi3qFun/Lp1+mFBfhlFB2rML4FTX+0fq6yJNaYuG/K6usiqo/n\n=QItZ\n-----END PGP MESSAGE-----",
1515
"fp": "CC7B10CE8D78010ABB043F8DB1C462E90012ECFE"
1616
}
1717
],

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ scratch/
217217

218218
# Cryptographic keys and certificates for testing that are not meant to be committed
219219
helmchart/**/*.crt
220-
helmchart/**/*.key
220+
**/*.key
221221
helmchart/**/*.csr
222222
helmchart/**/*.srl
223223
helmchart/**/server.conf

.vscode/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
"request": "launch",
1111
"program": "${workspaceFolder}/middleware/sql_to_arc/src/middleware/sql_to_arc/main.py",
1212
"console": "integratedTerminal",
13-
"envFile": "${workspaceFolder}/dev_environment/.env",
13+
"envFile": "${workspaceFolder}/.env",
1414
"args": [
1515
"-c",
16-
"${workspaceFolder}/dev_environment/config.yaml"
16+
"${workspaceFolder}/dev_environment/debug_config.yaml"
1717
]
1818
},
1919
{

dev_environment/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
log_level: INFO
1+
log_level: DEBUG
22

33
rdi: edaphobase
44
rdi_url: https://edaphobase.org

dev_environment/debug_config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
log_level: DEBUG
2+
3+
rdi: edaphobase
4+
rdi_url: https://edaphobase.org
5+
max_concurrent_arc_builds: 12
6+
7+
api_client:
8+
# NOTE: Change this to the external Middleware API URL
9+
api_url: "https://middleware.fairagro.net"
10+
timeout: 600
11+
client_cert_path: "dev_environment/client.crt"
12+
client_key_path: "dev_environment/client.key"
13+
verify_ssl: true
14+
15+
otel:
16+
log_console_spans: false

middleware/sql_to_arc/src/middleware/sql_to_arc/database.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
select,
1515
table,
1616
)
17-
from sqlalchemy.exc import ProgrammingError
17+
from sqlalchemy.exc import NoSuchTableError, ProgrammingError
1818
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
1919

2020
from middleware.sql_to_arc.models import (
@@ -66,7 +66,7 @@ async def _get_db_columns(conn: AsyncConnection, view_name: str) -> set[str] | N
6666
try:
6767
columns = await conn.run_sync(lambda sync_conn: inspect(sync_conn).get_columns(view_name))
6868
return {col["name"] for col in columns}
69-
except (ProgrammingError, sqlalchemy.exc.NoSuchTableError):
69+
except (ProgrammingError, NoSuchTableError):
7070
logger.warning('Table or view "%s" does not exist or is not accessible.', view_name)
7171
return None
7272

@@ -184,7 +184,8 @@ def _validate_and_map(
184184
entity_name: str,
185185
) -> RowModel | None:
186186
try:
187-
return model.model_validate(dict(row))
187+
validated: RowModel = model.model_validate(dict(row))
188+
return validated
188189
except ValidationError as error:
189190
logger.warning("Skipping %s due to validation error: %s", entity_name, error)
190191
return None
@@ -198,9 +199,13 @@ async def stream_investigations(
198199
view_name = InvestigationRow.__view_name__
199200
try:
200201
async with self.engine.connect() as conn:
201-
# Use SQLAlchemy select() and limit() for dialect-agnosticism
202-
t = table(view_name)
203-
stmt = select(t).execution_options(stream_results=True)
202+
# Use literal_column("*") to ensure SQLAlchemy generates 'SELECT *'
203+
# instead of '"vInvestigation"."*"'
204+
stmt: sqlalchemy.Select[Any] = (
205+
select(sqlalchemy.literal_column("*"))
206+
.select_from(table(view_name))
207+
.execution_options(stream_results=True)
208+
)
204209
if limit:
205210
stmt = stmt.limit(limit)
206211

@@ -235,10 +240,14 @@ async def _stream_by_investigation(
235240
view_name = model.__view_name__
236241
try:
237242
async with self.engine.connect() as conn:
238-
# Use SQLAlchemy select() and in_() for dialect-agnosticism
239-
t = table(view_name)
243+
# Use literal_column("*") to select all columns
240244
c_inv_ref: sqlalchemy.ColumnElement[Any] = column("investigation_ref")
241-
stmt = select(t).where(c_inv_ref.in_(investigation_ids)).execution_options(stream_results=True)
245+
stmt: sqlalchemy.Select[Any] = (
246+
select(sqlalchemy.literal_column("*"))
247+
.select_from(table(view_name))
248+
.where(c_inv_ref.in_(investigation_ids))
249+
.execution_options(stream_results=True)
250+
)
242251

243252
result = await conn.stream(stmt)
244253
async for row in result.mappings():
@@ -278,9 +287,14 @@ async def stream_annotation_tables(self, investigation_ids: list[str]) -> AsyncG
278287
view_name = "vAnnotationTable"
279288
try:
280289
async with self.engine.connect() as conn:
281-
t = table(view_name)
290+
# Use literal_column("*") to select all columns
282291
c_inv_ref: sqlalchemy.ColumnElement[Any] = column("investigation_ref")
283-
stmt = select(t).where(c_inv_ref.in_(investigation_ids)).execution_options(stream_results=True)
292+
stmt: sqlalchemy.Select[Any] = (
293+
select(sqlalchemy.literal_column("*"))
294+
.select_from(table(view_name))
295+
.where(c_inv_ref.in_(investigation_ids))
296+
.execution_options(stream_results=True)
297+
)
284298

285299
result = await conn.stream(stmt)
286300
async for row in result.mappings():

middleware/sql_to_arc/src/middleware/sql_to_arc/models.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from datetime import datetime
55
from typing import Any, ClassVar
66

7-
from pydantic import BaseModel, ConfigDict, Field, Json
7+
from pydantic import BaseModel, ConfigDict, Field, Json, model_validator
88
from pydantic_core import PydanticUndefined
99

1010
logger = logging.getLogger(__name__)
@@ -60,6 +60,30 @@ class BaseRow(BaseModel):
6060

6161
model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True)
6262

63+
@model_validator(mode="before")
64+
@classmethod
65+
def apply_spec_overrides(cls, data: Any) -> Any:
66+
"""Replace NULL (None) with default values for fields that allow spec overrides."""
67+
if not isinstance(data, dict):
68+
return data
69+
70+
for field_name, field_info in cls.model_fields.items():
71+
# Check if value is explicitly None (SQL NULL)
72+
if data.get(field_name) is None:
73+
json_extra = field_info.json_schema_extra
74+
allow_override = json_extra.get("spec_override", False) if isinstance(json_extra, dict) else False
75+
76+
# If override is allowed, replace with the field's default value
77+
if allow_override:
78+
# Only apply if a default exists
79+
if field_info.default is not PydanticUndefined:
80+
data[field_name] = field_info.default
81+
elif field_info.get_default(call_default_factory=True) is not None:
82+
# Pydantic's get_default handles factory calls safely
83+
data[field_name] = field_info.get_default(call_default_factory=True)
84+
85+
return data
86+
6387

6488
class InvestigationRow(BaseRow):
6589
"""Pydantic model for investigation database rows."""

middleware/sql_to_arc/src/middleware/sql_to_arc/py.typed

Whitespace-only changes.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Tests for database SQL fixes."""
2+
3+
from collections.abc import AsyncGenerator
4+
from unittest.mock import AsyncMock, MagicMock
5+
6+
import pytest
7+
8+
from middleware.sql_to_arc.database import Database
9+
from middleware.sql_to_arc.models import StudyRow
10+
11+
12+
@pytest.mark.asyncio
13+
async def test_stream_by_investigation_selects_all_columns() -> None:
14+
"""Verify _stream_by_investigation uses literal_column('*') for correct column capture."""
15+
# This tests the SQLAlchemy SQL generation fix we applied
16+
db = Database("postgresql://dummy")
17+
# Mock the engine and connection
18+
mock_engine = MagicMock()
19+
mock_conn = AsyncMock()
20+
db.engine = mock_engine
21+
mock_engine.connect.return_value.__aenter__.return_value = mock_conn
22+
23+
# Mock stream to return an empty async iterator
24+
async def async_iter() -> AsyncGenerator[None, None]:
25+
if False:
26+
yield # Trick to make it an async generator
27+
28+
mock_result = MagicMock()
29+
mock_result.mappings.return_value = async_iter()
30+
mock_conn.stream.return_value = mock_result
31+
32+
# Call _stream_by_investigation
33+
ids = ["INV001"]
34+
# We consume it
35+
async for _ in db._stream_by_investigation(StudyRow, ids, "study"):
36+
pass
37+
38+
# Inspect the call to conn.stream()
39+
assert mock_conn.stream.called
40+
stmt = mock_conn.stream.call_args[0][0]
41+
42+
# Verify the statement selects *
43+
# In SQLAlchemy 2.0, the statement object should show the column as *
44+
# stmt.selected_columns contains the columns in the SELECT clause
45+
# literal_column("*") is translated to textual "*"
46+
47+
# Check if any column is literal "*"
48+
columns = list(stmt.selected_columns)
49+
column_names = [str(c) for c in columns]
50+
assert "*" in column_names or '"*"' in column_names or any("*" in name for name in column_names)
51+
52+
# Also check that it's from the correct table
53+
assert StudyRow.__view_name__ in str(stmt)
54+
55+
56+
@pytest.mark.asyncio
57+
async def test_stream_investigations_selects_all_columns() -> None:
58+
"""Verify stream_investigations uses literal_column('*') correctly."""
59+
db = Database("postgresql://dummy")
60+
mock_engine = MagicMock()
61+
mock_conn = AsyncMock()
62+
db.engine = mock_engine
63+
mock_engine.connect.return_value.__aenter__.return_value = mock_conn
64+
65+
async def async_iter() -> AsyncGenerator[None, None]:
66+
if False:
67+
yield
68+
69+
mock_result = MagicMock()
70+
mock_result.mappings.return_value = async_iter()
71+
mock_conn.stream.return_value = mock_result
72+
73+
mock_stats = MagicMock()
74+
async for _ in db.stream_investigations(mock_stats):
75+
pass
76+
77+
assert mock_conn.stream.called
78+
stmt = mock_conn.stream.call_args[0][0]
79+
80+
columns = list(stmt.selected_columns)
81+
column_names = [str(c) for c in columns]
82+
assert "*" in column_names or '"*"' in column_names
83+
assert "vInvestigation" in str(stmt)

0 commit comments

Comments
 (0)