Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c9dea8c
Fix the schema not being properly bound to the engines by using direc…
nicoloesch Aug 28, 2026
1c3477d
Adapt to newr test mechanism
nicoloesch Aug 31, 2026
33088b1
Adhere to process isolation for postgres DBs
nicoloesch Sep 1, 2026
60f898d
Use outstanding autocommit
nicoloesch Sep 1, 2026
310d5c2
Update CI
nicoloesch Sep 1, 2026
5f27e38
Updated CI
nicoloesch Sep 3, 2026
260a923
Small fixes to autocommit and where the staging schema is registered
nicoloesch Sep 4, 2026
2228f6b
oa-configurator dialect changes
nicoloesch Sep 7, 2026
ab813c8
cdm_schema renames
nicoloesch Sep 15, 2026
ba3139f
Funnel role into each method for correct schema resolution
nicoloesch Sep 15, 2026
ede7911
Extend to non-primary tagged tables, split connection test
nicoloesch Sep 15, 2026
808d11f
Remove unused import
nicoloesch Sep 15, 2026
d2ea59f
Have a scoped_session fixture
nicoloesch Sep 16, 2026
14f221f
Merge remote-tracking branch 'origin/main' into schema-translate-map
nicoloesch Sep 16, 2026
7f14a91
Remove redundant check for dialect and tie it into contextmanager _as…
nicoloesch Sep 16, 2026
9a333a2
Obtain role_of_table from oa-configurator, remove explicit primary fa…
nicoloesch Sep 21, 2026
7ad3ad8
Fix missing test fixes that weren't committed
nicoloesch Sep 21, 2026
a1e22b4
Addressed PR comments
nicoloesch Sep 21, 2026
53de2cf
Pull out role following oa-configurator changes
nicoloesch Sep 22, 2026
7bd1f58
Quick edit to docstring
nicoloesch Sep 23, 2026
f52bb2a
Follow-up from internal review
nicoloesch Sep 23, 2026
a24e453
Disambiguate physical schema from schema tag
nicoloesch Sep 23, 2026
f5083aa
Docstring adaptations
nicoloesch Sep 24, 2026
fcb6a33
Updated Docs
nicoloesch Sep 24, 2026
64fe41a
Update to most recent oa-config changes, catch unregistered staging s…
nicoloesch Oct 1, 2026
b533dac
Updated tests
nicoloesch Oct 2, 2026
3a6ec13
Schema_tag routing as a result of private review, smaller helpers
nicoloesch Oct 7, 2026
0b2fbb8
Remove unncessary bind
nicoloesch Oct 7, 2026
5f5d5af
Polishing of docstrings
nicoloesch Oct 7, 2026
48d5d2e
Fix ty
nicoloesch Oct 7, 2026
7ba18c0
Address PR comments
nicoloesch Oct 9, 2026
176929c
Include vocab routing changes
nicoloesch Oct 9, 2026
77e7e8b
Route loaders by mapper for split sessions; fix create_tables for spl…
gkennos Oct 10, 2026
b74a1fd
updating configurator dependency
gkennos Oct 10, 2026
78d5481
cicd changes and reject negative merge batch sizes
gkennos Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ on:
jobs:
label-gate:
uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/label-gate.yml@main
build-test:
uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test-postgres.yml@main
build-test-sqlite:
uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main
build-test-postgres:
uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test-postgres-v2.yml@main
with:
postgres-db: orm_loader_test
setup-commands: |
uv run omop-config configure orm_loader \
--set test_orm_db.kind=cdm \
--set test_orm_db.connection.dialect=postgresql+psycopg \
--set test_orm_db.connection.host=localhost \
--set test_orm_db.connection.port=5432 \
--set test_orm_db.connection.user=test \
--set test_orm_db.connection.password=test \
--set test_orm_db.connection.database_name=orm_loader_test \
--set test_orm_db.connection.test_only=true \
--set test_orm_db.schema_name=public
--set test_orm_db_pg.kind=cdm \
--set test_orm_db_pg.connection.dialect=postgresql+psycopg \
--set test_orm_db_pg.connection.host=localhost \
--set test_orm_db_pg.connection.port=5432 \
--set test_orm_db_pg.connection.user=test \
--set test_orm_db_pg.connection.password=test \
--set test_orm_db_pg.connection.database_name=orm_loader_test \
--set test_orm_db_pg.connection.test_only=true \
--set test_orm_db_pg.cdm_schema=public
2 changes: 1 addition & 1 deletion docs/loaders/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ on globals or implicit configuration.
| `chunksize` | Optional chunk size |
| `normalise` | Whether to cast values to ORM types |
| `dedupe` | Whether to deduplicate incoming data |
| `quote_mode` | CSV quoting mode for PostgreSQL fast-path loading |
| `quote_mode` | CSV quoting mode, resolved to the same concrete mode on both the PostgreSQL COPY fast-path and the pandas ORM fallback |

::: orm_loader.loaders.data_classes.LoaderContext

Expand Down
12 changes: 12 additions & 0 deletions docs/registry/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,15 @@ Validates primary key presence, nullability, and alignment with specs.
Validates structural correctness of foreign key definitions.

::: orm_loader.registry.validation.ForeignKeyShapeValidator

---

## `always_on_validators()`

Returns the standard bundle of all four validators above
(`ColumnPresenceValidator`, `ColumnNullabilityValidator`,
`PrimaryKeyValidator`, `ForeignKeyShapeValidator`) as a ready-made list, for
callers that want the default validation set without constructing each one
individually.

::: orm_loader.registry.validation_presets.always_on_validators
38 changes: 27 additions & 11 deletions docs/tables/mat_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,44 @@ PatientSummaryMV.refresh_mv(engine, concurrently=True)

This is fail-closed by design. An index created manually outside `__mv_indexes__` does not satisfy the mixin's declaration contract; declare it in the class even if another migration is responsible for creating it. Expressions, partial indexes, and other index forms are outside this simple contract and should not be represented as `MaterializedViewIndex` entries.

By default, `schema=None` leaves the view name unqualified. PostgreSQL resolves that name through the connection's `search_path`, matching the behavior of existing callers. Pass `schema="reporting"` only when the caller intentionally wants an explicit schema-qualified target.
The view's physical schema comes from the bound connection's own `schema_translate_map`. `create_mv()`/`refresh_mv()`/`drop_mv()` resolve it via `oa_configurator.physical_schema_of()` before ever reaching the backend, which only ever sees an already-resolved physical schema string.

The schema tag itself comes from one of three places, in order:

1. an explicit `schema_tag=` argument, for a one-off override;
2. when the class is also declaratively mapped (combined with a `Base`, with a real `__table__`), that table's own `schema` -- set the schema the same way as any other mapped table, via `__table_args__ = {"schema": ...}`, and the materialized view follows it automatically;
3. `__mv_schema_tag__` (defaults to `"primary"`), only consulted for a Core-only declaration with no mapped table to derive anything from.

```python
# Existing/default behavior: search_path resolves the target.
# Core-only: no Base, no mapped table, so __mv_schema_tag__ is what resolves it.
class VocabSummaryMV(MaterializedViewMixin):
__mv_name__ = "vocab_summary"
__mv_select__ = ...
__mv_schema_tag__ = Role.VOCAB.value # resolves via the connection's vocab schema

# Uses __mv_schema_tag__ ("primary" by default) via the connection's own schema_translate_map.
RecentObservationMV.create_mv(engine)

# Explicit schema: the target is quoted and schema-qualified.
RecentObservationMV.create_mv(engine, schema="reporting")
RecentObservationMV.refresh_mv(engine, schema="reporting")
# Override for one call.
RecentObservationMV.create_mv(engine, schema_tag=Role.VOCAB.value)

# Declaratively mapped: the mapped table's own schema is authoritative, no
# __mv_schema_tag__ needed (or consulted) at all.
class VocabPatientSummaryMV(Base, MaterializedViewMixin):
__mv_name__ = "vocab_patient_summary"
__mv_select__ = ...
__tablename__ = "vocab_patient_summary"
__table_args__ = {"schema": Role.VOCAB.value}
patient_id = sa.Column(sa.Integer, primary_key=True)
```

Explicit schema targets are quoted component by component. This matters for embedded quotes, spaces, and mixed-case identifiers. It also means an unqualified mixed-case name and the same name passed with `schema=` can address different PostgreSQL relations. Keep schema selection at the call site and do not assume that this API provides `schema_translate_map`, role-token, or general multi-schema behavior.
Every generated identifier is quoted through `oa_configurator.qualified()`, which quotes each component only when the dialect actually requires it (reserved words, mixed case, embedded quotes or spaces) — the same behavior every other Core-built query in this stack has.

## Failure handling and backend support

The built-in implementation is PostgreSQL-oriented. SQLite rejects materialized-view operations with `NotImplementedError`; this is intentional, not an emulation using ordinary views.

`drop_mv()` and declared-index creation wrap execution failures in `MaterializationError`.
`create_mv()`, `drop_mv()`, and declared-index creation all wrap execution failures in `MaterializationError`.

## API reference

Expand Down Expand Up @@ -166,7 +186,3 @@ The built-in implementation is PostgreSQL-oriented. SQLite rejects materialized-
::: orm_loader.mappers.ConcurrentRefreshNotEligibleError
options:
heading_level: 3

::: orm_loader.mappers.UnsupportedMaterializationDialectError
options:
heading_level: 3
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ license = { text = "Apache-2.0" }
requires-python = ">=3.12"
dependencies = [
"chardet>=5.2.0",
"oa-configurator>=1.0.0,<2.0.0",
"oa-configurator>=2.0.0,<3",
"pandas>=2.3.3",
"pyarrow>=23.0.1",
"sqlalchemy>=2.0.45",
Expand Down Expand Up @@ -50,7 +50,7 @@ postgres = [
"psycopg[binary]>=3.2",
]
dev = [
"oa-configurator[postgres]>=1.0.0,<2.0.0",
"oa-configurator[postgres]>=2.0.0,<3",
"pytest>=9.0.3",
"ty>=0.0.59",
"ruff>=0.14.11",
Expand Down Expand Up @@ -82,7 +82,7 @@ testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-ra"
addopts = "-ra -m 'not db_dialect'"
Comment thread
gkennos marked this conversation as resolved.

[tool.pyright]
reportMissingTypeStubs = false
11 changes: 8 additions & 3 deletions src/orm_loader/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from .postgres import PostgresBackend
from .resolve import resolve_backend
from .sqlite import SQLiteBackend
from .base import BackendCapabilities, DatabaseBackend, STAGING_SCHEMA, Dialect
from .base import (
BackendCapabilities,
DatabaseBackend,
Dialect,
STAGING_SCHEMA,
staging_schema_claim,
)
from ..mappers.materialised_view_errors import (
ConcurrentRefreshNotEligibleError,
MaterializationError,
MaterializationFailure,
MaterializationOperation,
UnsupportedMaterializationDialectError,
)

__all__ = [
Expand All @@ -21,6 +26,6 @@
"PostgresBackend",
"STAGING_SCHEMA",
"SQLiteBackend",
"UnsupportedMaterializationDialectError",
"resolve_backend",
"staging_schema_claim",
]
Loading
Loading