0030: typing.Protocol for the pluggable seams¶
Status: accepted
Date: 2026-10-07T09:05:52-03:00
Amended: 2026-10-15T09:35:58-03:00, CdcClient is internal and may gain methods in a minor; Backend stays the public, frozen protocol (amendment 1)
Context¶
The library has two seams. CdcClient is what the data source needs from SQL Server:
SqlCdcClient implements it in T-SQL, the fake backend's FakeCdcClient from files
(ADR 0006). Backend is what SqlCdcClient needs
from a driver: mssql-python and arrow-odbc (ADR 0003).
Through 0.3 both were ABCs, so a class was one only by inheriting, and a type checker rejected
a backend of one's own that did not. Neither was exported.
LSNs cross every boundary as hex strings (invariant 6) and were typed str, the same type as
a capture instance or a commit time.
Decision¶
CdcClientandBackendaretyping.Protocols,runtime_checkable, exported frommssql_cdcand listed in the API reference. A class with their methods is one, inheriting or not;isinstancechecks that it has them, by name (a type checker checks the signatures).SqlCdcClient(backend)takes anyBackend;make_clientreturns aCdcClient.- The protocols are the 0.3 classes themselves, not new ones beside them. A class that
subclasses a protocol inherits its method bodies, and Python enforces its abstract methods
as it did the ABC's (a protocol's metaclass derives from
ABCMeta):class X(Backend)that definesbatchesstill getsscalarandclose, and one that does not still fails to instantiate. So subclassing stays supported, with nothing to deprecate. - A method with a body is a default for subclasses and still a member a class that does not
inherit must define. The body is never only
pass,...or a docstring, which a type checker reads as abstract in a subclass of a protocol: a no-op default is a barereturn. Lsn = NewType("Lsn", str)inmssql_cdc.lsn, exported:normalize,from_intandZERO_LSNareLsns, and so is every LSN the client returns, itsSourceTable,CaptureInstanceandDdlChangeincluded. At run time it is the samestr.- Parameters still take any
str: the LSNs a caller holds come from the offsets' JSON and the facts table, where they are plain strings, and requiringLsnwould make every such call wrap them in a check that does nothing at run time. SoLsnmarks what is canonical on the way out, and a function of one's own can ask for it.Offset["lsn"]staysstr: the offset is JSON (invariant 1). - A member added to a protocol breaks a class that implements it without inheriting: a minor's change, listed under "Breaking" (ADR 0021). A body for it keeps subclasses working.
- The data source still builds its client from its options in every task: the
backendoption names a built-in backend only, and a backend of one's own runs underSqlCdcClientoutside a stream.
Considered:
- Protocols beside the ABCs, the ABCs kept one release as bases and removed in 0.5 (the
roadmap's first plan). The names are taken:
client.Backendwould be either the ABC, andmssql_cdc.Backendanother class of the same name, or the protocol, whose bodilessscalara 0.3 subclass would inherit and silently getNonefrom. Explicit subclassing of a protocol is part of PEP 544, so one class serves both. - A backend named by an option (
backend=package.module:Class), so the data source builds a user's backend in every task. A new behaviour, not a type: a later release, when someone needs it. Lsnon parameters too. It flags a non-canonical string only where every caller already normalizes, and costs a wrapper at each of them.
Consequences¶
- A backend of one's own type-checks and passes
isinstancewithout a base class (tests/typing_protocols.py,tests/test_client_sql.py). - Code that subclassed
client.Backendorclient.CdcClientruns unchanged. A subclass that annotated an LSN return asstris now flagged by a type checker, asLsnis narrower; at run time nothing changed. - The protocols' methods are public API: renaming or removing one, or changing its signature, is a break listed in the changelog.
Amendment 1: CdcClient is internal¶
1.0 freezes what the reference lists (ADR 0021
amendment 11), and a member added to a protocol breaks a class that implements it without
inheriting. CdcClient grows with the features: the capture instance switch and the
chunked snapshot each added methods to it
(ADR 0023,
ADR 0028). Frozen, every such
feature would wait for 2.0. And no user's client plugs in: the data source builds its own
from its options in every task, so a CdcClient of one's own runs nowhere the library
calls it.
CdcClientleavesmssql_cdc.__all__and the API reference before 1.0.0rc1. It stays inmssql_cdc.client, internal: a minor release may add methods to it, or change it otherwise. Imported frommssql_cdcit still works until 2.0, with aDeprecationWarning, as the names amendment 11 of ADR 0021 took out of the namespace.Backendstays public and frozen: what a driver needs to giveSqlCdcClientis small and settled, and a method added to it waits for 2.0.Lsnstays public too.SqlCdcClient,FakeCdcClientandmake_clientkeep implementing and returningCdcClient;tests/typing_protocols.pyandtests/test_client_sql.pykeep checking both protocols.