Repository navigation
Commit 6fe9527
authored
feat(sqlserver): support client certificates on the ODBC connection string (#6)
* perf(sqlserver): resolve backend exports lazily so expressions import without a driver
Every backend __init__ imported all of its submodules eagerly, and
.backend imports the DB driver. Python executes a parent package's
__init__ before any submodule, so `impl.sqlserver.expression` could not
be imported without the driver present - even though no expression class
touches it. Auditing the expression namespace needed a working driver;
pyodbc additionally needs the OS libodbc.so, so a plain pip install was
not enough and sqlserver dropped out of the audit entirely.
Public names are now resolved on first access through a PEP 562 module
__getattr__, with the name -> (module, attribute) map derived from the
existing import statements.
The exported surface is unchanged: attribute access, __all__, dir() and
`from impl.sqlserver import *` all behave as before, and the previous
eager imports move into an `if TYPE_CHECKING:` block so mypy and IDEs
still resolve them. The existing __getattr__ is preserved as
_optional_dependency_getattr, so the async backends keep raising an
install hint for aioodbc instead of a bare ImportError.
Importing the package, or its .expression submodule, no longer loads the
driver; the driver error now surfaces at first use of the name that needs
it, where it is actionable.
55 public names verified to resolve.
* refactor(backend): make impl.<backend> an open package
Drop the top-level impl/<backend>/__init__.py so the directory becomes a
PEP 420 namespace portion and no longer re-exports every public name. Each
name is now imported from the sub-package that defines it, so a symbol has
one public path instead of two.
The facade also made "expression is importable on its own" unenforceable:
importing impl.<backend>.expression executed the parent __init__, which
pulled in the driver. With the file gone, the sub-packages stand alone by
construction rather than by a lazy __getattr__ that each backend had to
maintain separately.
This is a breaking change for anyone importing
`from rhosocial.activerecord.backend.impl.<backend> import X`; the
replacement is `...impl.<backend>.<module> import X`.
* fix(providers): resolve the async backend across sibling modules
The provider gave the worker tasks a single module path and the task code
looked for the async counterpart one level up. That was enough while the sync
and async classes shared the impl.<backend> facade; with the facade gone they
live in different modules and the one-level lookup fell through to the sync
class, so an async worker would silently run the sync backend.
Walk up the chain first, then try the backend's own async_backend module. No
package scanning is involved: both candidates are named explicitly.
* refactor(backend): keep both backend classes in impl.<backend>.backend
Move the sync and async classes into a backend/ package with the fixed layout:
backend/backend.py and backend/async_backend.py. Eight backends kept them as
two modules beside backend/, and sqlite and postgres used backend/sync.py, so
the class locations differed three ways between backends.
With the layout fixed the worker tasks derive the async class from the sync
module's own package instead of searching for it, so the
_find_async_backend helper is gone.
* fix(backend): correct relative import depth inside backend/
The move into backend/ shifted every relative import by one level, and the
function-local ones were missed, so impl.bigquery could not reach its adapters
module. Resolve each target against the filesystem instead of counting dots.
* refactor(backend): re-export the async class from the backend package
impl.<backend>.backend already re-exported the sync class, so leaving the async
one out made the two classes reachable by different rules. Export both.
The eight backends whose async driver is a hard requirement import it
eagerly. SQLite and SQL Server keep a PEP 562 __getattr__, because aiosqlite
and aioodbc ship as the [async] extra and importing either eagerly would make
the async class a requirement for sync-only users.
* fix(test): patch the backend class on the module the CLI reads
The CLI tests monkeypatched SQLServerBackend on impl.sqlserver, which no longer
carries the name now that the directory is a namespace. Patch
impl.sqlserver.backend instead, and the async class on
impl.sqlserver.backend.async_backend, which is where each is defined.
* fix(test): patch the async backend class on the module the CLI reads
The remaining multi-line monkeypatch still targeted impl.sqlserver.backend for
AsyncSQLServerBackend, but the CLI imports it from
impl.sqlserver.backend.async_backend inside the handler, so the patch never
reached it and the real backend was constructed.
* fix(sqlserver): repair the import line the package move truncated
The two CLI demos embed runnable Python as a string literal, and the import
migration rewrote the module path inside it without keeping the statement
terminator, leaving the string unterminated:
"from rhosocial.activerecord.backend.impl.oracle.backend.async_backend import OracleBackend
ast.parse rejects all four demo modules with "unterminated string literal", so
they could not even be imported, let alone run. The migration also dropped the
trailing OracleConnectionConfig from the original single-statement import; the
surrounding lines already import it from its own module, so restoring the ";"
recovers the intended statements.
The SQL Server demos additionally reached SQLServerConnectionConfig through the
impl.sqlserver facade, which no longer exists now that the package is open, so
they now name impl.sqlserver.config like every other backend.
Verified: ast.parse is clean across every src/ and tests/ module in both repos.
* chore: remove two legacy re-export shims shadowed by same-named packages
mixins.py and protocols.py each sat next to a package of the same name, holding
nothing but a re-export of that package's contents. A regular package takes
precedence over a same-named module, so both shims were unreachable: importing
impl.sqlserver.mixins or impl.sqlserver.protocols always resolved to the
package's __init__.py. Each re-export list is a strict subset of the package's,
so nothing that resolved through a shim can break.
Also corrects the path comment on mixins/ddl_table.py, which still named the
file before it was renamed from table.py.
* docs: rewrite the README and correct the .claude/ knowledge base
Removed the __partition__ model attribute from the partitioning example: no
such attribute exists, and table partitioning is in fact unsupported by this
backend -- add_partition, attach_partition, detach_partition and the three
partitioning switches are all False at every probed version. The generated
version table is what surfaces that. Corrected the core dependency floor,
documented as >=1.0.0 and unresolvable against the 1.0.0.devN pre-release
line.
In the agent docs: replaced the invented DatabaseCapabilities /
_initialize_capabilities / requires_capability system with the actual
supports_* protocol switches, and corrected the PYTHONPATH guidance, which
omitted tests.
* feat(sqlserver): support client certificates on the ODBC connection string
encrypt and trust_server_certificate only cover server-side encryption,
so a client certificate could not be presented at all. Add
client_certificate, client_key, client_key_password and
host_name_in_certificate to SQLServerConnectionConfig and emit them as the
ClientCertificate/ClientKey/ClientKeyPassword/HostNameInCertificate ODBC
keywords from build_connection_string(), with the same four names accepted
as backend kwargs.
validate_config() now rejects a client_key given without a
client_certificate, so the mismatch surfaces as a clear ValueError instead
of an opaque driver failure at connect time.
* chore: rename changelog fragment to PR #6
* ci: unify the changelog fragment rules
Replace the six divergent changelog-check variants with one implementation
that encodes the documented merge rules.
feature/*|fix/* -> release/*|maint/*|main:
- the PR must add at least one changelog.d/<number>.<type>.md
- every added fragment's <number> must equal the PR number
- fragments already present from previously merged branches are ignored,
since only files ADDED by this PR are considered
- escape hatches ([trivial] title, skip-changelog label, docs: update
CHANGELOG) waive only the "at least one" requirement; the
number-must-match rule is always enforced on fragments that were added
release/* -> main:
- CHANGELOG.md must have changed (content is not inspected)
- changelog.d/ must hold no fragments at the merge commit
- no escape hatches apply to this path
The number-must-match rule was the one documented requirement that none of
the previous variants implemented. Two latent bugs are also fixed: the
paths-ignore of "**.md" meant a CHANGELOG.md-only push never ran the check at
all, and three backends skipped release merges outright instead of applying
the release rules.
* ci: reject feature/fix branches that target main directly
A feature/fix branch merges into a release or maintenance branch; only
release/* merges into main. Opening a second PR at main from the same
branch is a duplicate, and it also breaks the changelog fragment, which is
numbered for exactly one PR.
This pattern already cost a wasted PR in six repositories (a ->release PR
merged alongside a ->main PR that was then closed), and the fragment
numbering check only exposed it indirectly, by failing the leftover PR.
hotfix/* -> main stays allowed; the version control policy calls for
hotfixes to be merged back to main and the active release branch.1 parent 61ab594 commit 6fe9527
67 files changed
Lines changed: 637 additions & 623 deletions
File tree
- .claude
- commands
- .github/workflows
- changelog.d
- src/rhosocial/activerecord/backend/impl/sqlserver
- backend
- cli
- examples
- cli
- connection
- ddl
- insert
- named_expressions
- named_procedures
- query
- schema_diff
- transaction
- types
- mixins
- tests
- providers
- rhosocial/activerecord_sqlserver_test/feature
- backend
- cli
- connection
- query
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
117 | | - | |
| 117 | + | |
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
| |||
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
152 | | - | |
153 | | - | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
154 | 155 | | |
155 | 156 | | |
156 | 157 | | |
| |||
175 | 176 | | |
176 | 177 | | |
177 | 178 | | |
178 | | - | |
| 179 | + | |
| 180 | + | |
179 | 181 | | |
180 | 182 | | |
181 | 183 | | |
| |||
197 | 199 | | |
198 | 200 | | |
199 | 201 | | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | 202 | | |
205 | 203 | | |
206 | 204 | | |
| |||
241 | 239 | | |
242 | 240 | | |
243 | 241 | | |
244 | | - | |
| 242 | + | |
245 | 243 | | |
246 | | - | |
247 | | - | |
| 244 | + | |
| 245 | + | |
248 | 246 | | |
249 | 247 | | |
250 | 248 | | |
| |||
366 | 364 | | |
367 | 365 | | |
368 | 366 | | |
369 | | - | |
370 | | - | |
371 | | - | |
372 | | - | |
| 367 | + | |
| 368 | + | |
373 | 369 | | |
374 | | - | |
| 370 | + | |
375 | 371 | | |
376 | 372 | | |
377 | 373 | | |
378 | 374 | | |
379 | | - | |
380 | | - | |
381 | | - | |
382 | | - | |
383 | | - | |
384 | | - | |
385 | | - | |
386 | | - | |
387 | | - | |
388 | | - | |
| 375 | + | |
| 376 | + | |
389 | 377 | | |
390 | | - | |
391 | | - | |
392 | | - | |
393 | | - | |
394 | | - | |
395 | | - | |
396 | | - | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
397 | 381 | | |
398 | 382 | | |
399 | 383 | | |
| |||
565 | 549 | | |
566 | 550 | | |
567 | 551 | | |
568 | | - | |
| 552 | + | |
569 | 553 | | |
570 | 554 | | |
571 | 555 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
| 59 | + | |
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
| 68 | + | |
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| |||
291 | 291 | | |
292 | 292 | | |
293 | 293 | | |
294 | | - | |
| 294 | + | |
295 | 295 | | |
296 | 296 | | |
297 | 297 | | |
| |||
414 | 414 | | |
415 | 415 | | |
416 | 416 | | |
417 | | - | |
| 417 | + | |
418 | 418 | | |
419 | 419 | | |
420 | 420 | | |
421 | 421 | | |
422 | | - | |
| 422 | + | |
423 | 423 | | |
424 | 424 | | |
425 | 425 | | |
| |||
603 | 603 | | |
604 | 604 | | |
605 | 605 | | |
606 | | - | |
607 | | - | |
608 | | - | |
609 | | - | |
610 | | - | |
611 | | - | |
612 | | - | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
613 | 612 | | |
614 | 613 | | |
615 | 614 | | |
| |||
687 | 686 | | |
688 | 687 | | |
689 | 688 | | |
690 | | - | |
| 689 | + | |
691 | 690 | | |
692 | 691 | | |
693 | 692 | | |
| |||
705 | 704 | | |
706 | 705 | | |
707 | 706 | | |
708 | | - | |
| 707 | + | |
709 | 708 | | |
710 | 709 | | |
711 | 710 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | | - | |
| 69 | + | |
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
27 | | - | |
| 26 | + | |
| 27 | + | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
40 | | - | |
| 39 | + | |
| 40 | + | |
0 commit comments