Skip to content

refactor!: explicit backend api cleanup#6

Open
gkennos wants to merge 4 commits into
mainfrom
api_cleanup
Open

refactor!: explicit backend api cleanup#6
gkennos wants to merge 4 commits into
mainfrom
api_cleanup

Conversation

@gkennos
Copy link
Copy Markdown
Member

@gkennos gkennos commented May 14, 2026

BREAKING CHANGE: known breaking change - removal of the dialect argument from _merge_replace() and _merge_upsert().

Opinionated backend now means that in the unlikely event that someone was using this on some other dialect, it will no longer work, but normal usage on SQLite and PostgreSQL is intended to remain functionally the same.

@gkennos gkennos requested review from Copilot and nicoloesch May 14, 2026 22:26
@gkennos gkennos self-assigned this May 14, 2026
@gkennos gkennos added the enhancement New feature or request label May 14, 2026
@gkennos gkennos changed the title explicit backend api cleanup refactor!: explicit backend api cleanup May 14, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors orm-loader to make database-specific behavior explicit via a backend abstraction, while updating the PostgreSQL path to use psycopg (v3) and expanding tests/docs accordingly.

Changes:

  • Introduces a DatabaseBackend contract with concrete SQLite/PostgreSQL implementations plus resolve_backend() dispatch.
  • Routes staging-table creation, fast-path loading, merge behavior, FK toggling, and materialized view operations through the resolved backend.
  • Updates packaging to add a postgres extra (psycopg[binary]), bumps version to 0.4.0, and adds/updates backend-focused test coverage and docs.

Reviewed changes

Copilot reviewed 34 out of 36 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
uv.lock Bumps package version and adds locked dependencies for new postgres extra (psycopg).
TODO.txt Adds future-facing notes about optional malformed text repair.
tests/models.py Adds an index to support index-management behavior in merge tests.
tests/conftest.py Switches PostgreSQL test DSN to SQLAlchemy psycopg driver.
tests/backends/test_sqlite_backend.py Adds unit tests for SQLite backend staging, FK toggling, merge SQL, and pragmas hooks.
tests/backends/test_postgres_backend.py Adds unit tests for Postgres backend staging, FK toggling, merge SQL, and materialized view SQL emission.
tests/backends/test_base_backend.py Adds contract tests for backend base class, capabilities, backend resolution, and import behavior without psycopg installed.
src/orm_loader/tables/typing.py Tightens typing (generics, TypedDict/Unpack) and adds index_strategy/updated merge signatures to the protocol.
src/orm_loader/tables/serialisable_table.py Improves type annotations for JSON/dict serialization helpers.
src/orm_loader/tables/orm_table.py Tightens ORMTableBase typing and reduces cast() usage.
src/orm_loader/tables/loadable_table.py Refactors staging/merge/index-management to use backend resolution and introduces index_strategy.
src/orm_loader/mappers/materialised_view_mixin.py Routes MV create/refresh through backend implementations and improves typing.
src/orm_loader/loaders/loading_helpers.py Updates PostgreSQL COPY helper to a psycopg3-style copy loop and introduces COPY_BLOCK_SIZE.
src/orm_loader/loaders/data_classes.py Removes stale commented-out dedupe code and trailing whitespace.
src/orm_loader/helpers/sqlite.py Replaces direct event-hook/pragmas logic with backend-delegated helpers for SQLite.
src/orm_loader/helpers/logging.py Simplifies log-level coercion and adds typing for formatter methods.
src/orm_loader/helpers/discovery.py Updates typing and changes defaulting behavior for the base parameter.
src/orm_loader/helpers/bulk.py Delegates FK toggling and bulk-load context behavior to the resolved backend; delegates replica-role engine context.
src/orm_loader/helpers/bootstrap.py Adds type annotations for bootstrap/schema creation helpers.
src/orm_loader/helpers/init.py Re-exports new/renamed SQLite helpers.
src/orm_loader/backends/sqlite.py Adds SQLite backend implementation (staging, merge SQL, FK toggling, connect hooks, FK error explanation, journal restore).
src/orm_loader/backends/resolve.py Adds backend resolver selecting a concrete backend based on SQLAlchemy dialect.
src/orm_loader/backends/postgres.py Adds Postgres backend implementation (unlogged staging, COPY fast-path, merge SQL, MV operations, replica-role context).
src/orm_loader/backends/base.py Adds backend base contract, capabilities, shared helpers, and a generic bulk-load context manager.
src/orm_loader/backends/init.py Exposes backend public API and resolver.
README.md Updates project description and documents backend support and staged ingestion approach.
pyproject.toml Bumps version, adds postgres optional dependency, and expands metadata/classifiers and tooling config.
docs/tables/mat_view.md Updates MV docs to reflect backend resolution and PostgreSQL-only support.
docs/tables/loadable_table.md Updates staged ingestion docs to reflect backend-specific behavior and index handling.
docs/loaders/loaders.md Clarifies loader responsibilities vs table/backend merge behavior and updates dedupe description.
docs/loaders/index.md Updates loader overview to emphasize staged loading and table/backend ownership of merge semantics.
docs/loaders/helpers.md Updates helper docs, including PostgreSQL COPY behavior and fallback behavior.
docs/loaders/context.md Documents quote_mode in loader context.
docs/index.md Updates project overview and explicitly documents current backend support limits.
CHANGELOG.md Adds 0.4.0 notes about psycopg and backend API cleanup.
.gitignore Adds _temp/ ignore entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyproject.toml Outdated
Comment on lines +191 to +200
@contextmanager
def engine_with_replica_role(self, engine: "Engine"):
@sa.event.listens_for(engine, "connect") # type: ignore[arg-type]
def _set_replica_role(
dbapi_conn: sa.engine.interfaces.DBAPIConnection,
_,
) -> None:
cur = dbapi_conn.cursor()
cur.execute("SET session_replication_role = replica")
cur.close()
if dbapi_connection.__class__.__module__.startswith("sqlite3"):
cursor = dbapi_connection.cursor()
cursor.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}")
cursor.execute(f"PRAGMA journal_mode = {self.journal_mode}")
Comment on lines +196 to +200
def configure_dbapi_connection(self, dbapi_connection: sa.engine.interfaces.DBAPIConnection) -> None:
if dbapi_connection.__class__.__module__.startswith("sqlite3"):
cursor = dbapi_connection.cursor()
cursor.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}")
cursor.execute(f"PRAGMA journal_mode = {self.journal_mode}")
Comment thread src/orm_loader/helpers/discovery.py
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 36 changed files in this pull request and generated 4 comments.

Comment thread pyproject.toml Outdated
dev = [
"pytest>=9.0.3",
"mypy>=1.19.1",
"pytest>=9.0.3",
Comment on lines +192 to +200
def engine_with_replica_role(self, engine: "Engine"):
@sa.event.listens_for(engine, "connect") # type: ignore[arg-type]
def _set_replica_role(
dbapi_conn: sa.engine.interfaces.DBAPIConnection,
_,
) -> None:
cur = dbapi_conn.cursor()
cur.execute("SET session_replication_role = replica")
cur.close()
Comment on lines +8 to +9
base: type[ModelT] = Base,
) -> type[ModelT] | None:
Comment on lines +14 to +20
Apply the default SQLite connection settings used by orm-loader.

This helper is kept for compatibility with older event-hook setups.
It delegates to ``SQLiteBackend.configure_dbapi_connection()``,
which may apply more than just foreign-key settings.
"""
del connection_record
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 39 changed files in this pull request and generated 4 comments.

bind: Engine | Connection,
) -> Iterator[Connection]:
if isinstance(bind, Engine):
with bind.connect() as conn:
Comment on lines +66 to +69
mapper: so.Mapper[Any] = sa.inspect(cls)
if not mapper:
raise TypeError(f"{cls.__name__} is not a mapped ORM class")
return cast(so.Mapper, mapper)
return mapper
Comment on lines +106 to +111
def restore_fk_check(
self,
session: so.Session,
previous_state: str | int,
) -> None:
session.execute(text(f"PRAGMA foreign_keys = {previous_state}"))
Comment on lines +95 to +96
session.execute(sa.text(f"SET session_replication_role = '{previous_state}'"))

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 39 changed files in this pull request and generated 4 comments.

self,
session: so.Session,
previous_state: str | int,
) -> None:
Comment on lines +90 to +96
def restore_fk_check(
self,
session: so.Session,
previous_state: str | int,
) -> None:
session.execute(sa.text(f"SET session_replication_role = '{previous_state}'"))

Comment on lines +193 to +214
def _set_replica_role(
dbapi_conn: sa.engine.interfaces.DBAPIConnection,
_,
) -> None:
cur = dbapi_conn.cursor()
cur.execute("SET session_replication_role = 'replica'")
cur.close()

sa.event.listen(engine, "connect", _set_replica_role)

try:
yield engine
finally:
sa.event.remove(engine, "connect", _set_replica_role)
with engine.connect() as conn:
conn = conn.execution_options(isolation_level="AUTOCOMMIT")
conn.execute(sa.text("SET session_replication_role = DEFAULT"))
role = conn.execute(
sa.text("SHOW session_replication_role")
).scalar()
if role != "origin":
raise RuntimeError("Failed to restore session_replication_role")
@@ -19,19 +19,15 @@ def _coerce_log_level(level: int | str) -> int:
if isinstance(level, int):
return level

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants