Skip to content

Commit 30fd9f4

Browse files
authored
feat: parallel testing infrastructure and SQL Server backend improvements (#2)
* feat: parallelize testsuite tests with database pooling for CI - wire the testsuite database pool and pooling providers so scenario tests run in parallel (pytest-xdist -n auto) against per-worker pooled databases instead of the shared scenario database - reset handler skips DROP/CREATE DATABASE when the slot is already clean - enable READ_COMMITTED_SNAPSHOT on pooled databases to avoid deadlock- induced rollback failures in concurrent transaction tests - install activerecord and testsuite from feature/parallel-testing in CI * ci: use -n 8 parallelism for server-type tests * fix(test): check for simultaneous double-hold instead of id uniqueness * feat: CLI async unification, --ssl param; sqlserver fixes (user/username unification, RICH_AVAILABLE removal, disconnect_async guard) * fix(test): pass --driver from scenario config in sqlserver blackbox conn_args The sqlserver CLI defaults --driver to ODBC Driver 18 for SQL Server, but CI runners only install ODBC Driver 17. The blackbox conn_args did not forward the scenario driver, so query/status/async tests failed with missing ODBC Driver 18. Forward config.driver via --driver so the correct installed driver is used. * feat(sqlserver): add named CLI examples and fix named-procedure CLI adapter - Fix cli/named_procedure.py adapter: remove invalid get_dialect/execute_query kwargs and ExecutionOptions import (core handle_named_procedure rejects them) - Add named_connections examples (development/production) with 6 connections - Add named_expressions order_expressions (import-safe, prepare_orders_demo helper) - Add named_procedures order_workflow (OrderProcessingProcedure, max_concurrency=1 for pyodbc single-connection concurrency limit) - Add cli demo scripts: named_connection_demo, named_query_demo, named_procedure_demo - Update examples README with new directories * test(expression): add serialization round-trip coverage for SQL Server expressions * test: add is_auto_increment/DDL regression tests Dialect-level tests (no DB needed) verifying: 1. is_auto_increment=True produces correct auto-increment DDL keyword 2. All standard types (Integer/Text/Boolean/VarChar/Timestamp) compile 3. BooleanType + DEFAULT True is accepted 4. TimestampType maps to correct backend-specific type Prevents recurrence of bugs found in rbac cross-backend testing. * feat(dialect): declare supports_schema; add cross-schema provider fixtures * test(provider): provision mixed default/non-default schema fixtures * ci(test): merge xdist coverage via pytest-cov; restore codecov fidelity The single uploading job ran coverage run under pytest-xdist without a combine step (59.49% -> 26.46%). Move to pytest-cov reporting and fail on upload errors. Investigation plan recorded locally under .claude/plan/2026-08-26. * test(backend): adapter suggestion cache behavior without a live server Locks the registered-type set (datetime/date/time/Decimal/UUID/dict/list) and documents that primitives intentionally rely on default binding; addresses the backend_mixin gap from the coverage plan. * test(sqlserver): CLI black-box suites, adapters and dialect version-grid units 238 cases across the four CLI entrypoints (parser contracts, fake-backend handle() outputs, error exits), 10 adapters bidirectional round-trips, and a SQL Server version grid for pagination/capability/temp-table/hint/MERGE rendering. Defects (XML dict adapter NameError, WKT commas, date-adapter dead branch) recorded in test comments only. * fix(adapters): module-level ET import and WKT point separators S1: hoist 'import xml.etree.ElementTree as ET' to module scope so SQLServerXMLAdapter dict rendering works instead of raising NameError. S2: _dict_to_wkt now separates coordinate pairs with commas (LINESTRING(1 2, 3 4), POLYGON((0 0, 0 1, 1 1, 0 0))) producing valid WKT. Anchor flip: NameError-pinning XML test replaced with positive round-trip assertions (text/nested/repeated nodes); LineString/Polygon expectations updated to comma-separated WKT. * fix(cli): honour encrypt/trust flags with defined precedence over --ssl S4: resolve_connection_config_from_args now reads --encrypt/--no-encrypt and --trust-server-certificate/--no-trust-server-certificate. Flags use a None sentinel default so explicit usage is detectable; explicit flags take precedence over --ssl derivation. The default --ssl auto no longer implies encrypt=True. Default matrix documented in the module docstring. Anchor flip: parser-contract tests assert None sentinel defaults; SSL mapping rewritten as a full five-mode matrix plus flag-combination precedence cases. * fix(adapters): date adapter branch ordering S3: SQLServerDateAdapter checks datetime before date (datetime is an isinstance of date) so to_database/from_database truncate datetimes to their date component; the value.date() branches are no longer dead code. Anchor flip: dead-branch pinning test replaced with datetime->date truncation assertions in both directions plus a date-only passthrough case. * docs(cli): clarify statement input forms S6: query --help epilog now states the three statement input forms (positional argument, -f/--file, stdin pipe) and that no -q short option exists by design. Documentation only; no behaviour change. * fix(backend): remove %s->? placeholder guessing at execute layer Unify placeholder handling per the get_parameter_placeholder() contract: callers provide native '?' markers and _prepare_sql_and_params only performs the SQL Server-specific table hint relocation. Update CRUD tests to use '?'. * test: use native '?' placeholders in remaining SQL Server tests Completes the placeholder unification contract: all remaining raw SQL in tests (transaction isolation, connection pool) switches from MySQL-style '%s' to SQL Server native '?' markers. * fix(introspection): parameterize SQL Server introspector SQL and harden SET statements - Convert all 7 _build_*_sql methods in the SQL Server introspector from f-string identifier interpolation to '?' placeholders, returning (sql, params) tuples consistent with the base IntrospectorMixin contract. - _build_primary_key_sql now returns (sql, params); update sync/async list_columns callers to pass params to executor.execute. - IDENT_CURRENT and DBCC CHECKIDENT now take table/seed as bound parameters instead of raw string embedding. - Add whitelist validation for SET LANGUAGE, SET DATEFORMAT, and SET DEADLOCK_PRIORITY (int range -10..10 or LOW/NORMAL/HIGH). * test: add SET whitelist and identifier quoting balance tests * test: fix SQL Server whitelist and bracket-balance security tests Test the actual whitelist constants (_SQLSERVER_LANGUAGES / _DATE_FORMATS / _DEADLOCK_PRIORITIES) and fix the bracket-balance assertion to correctly check that every embedded ] is escaped as ]]. * test: fix SQL Server security test dateformat casing and bracket-skip logic - SET DATEFORMAT validation lowercases input, so mdY is valid; keep only genuinely invalid formats in the negative case. - bracket-balance check now actually skips escaped ]] pairs via index skip. * fix(sqlserver): delegate set_deadlock_priority to backend whitelist validation Transaction manager's set_deadlock_priority now reuses the backend mixin's whitelist (-10..10 or LOW/NORMAL/HIGH) instead of bare f-string concat. * ci: update testsuite branch to release/v1.0.0.dev17 * ci: update rhosocial-activerecord branch to release/v1.0.0.dev30 * ci: update testsuite branch to release/v1.0.0.dev17 * test: add targeted coverage for low-coverage patch areas - cover backend_mixin validation branches (deadlock priority range/ whitelist, language whitelist, dateformat whitelist) and parameterized IDENT_CURRENT/CHECKIDENT queries via mocked cursors - cover introspector parameterized SQL builders (table/column/primary key/index/foreign key/view/trigger) offline - cover __main__.parse_args with custom argv - cover transaction manager set_deadlock_priority backend delegation * chore: add changelog fragment for PR #2
1 parent 25a7a6f commit 30fd9f4

53 files changed

Lines changed: 4683 additions & 229 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/test.yml‎

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- main
1313
- "release/v**"
1414
- "maint/**"
15+
workflow_dispatch:
1516

1617
permissions:
1718
contents: read
@@ -58,8 +59,8 @@ jobs:
5859
5960
- name: Install activerecord and testsuite from PyPI
6061
run: |
61-
pip install rhosocial-activerecord==1.0.0.dev29
62-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
62+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
63+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
6364
6465
- name: Create test database
6566
run: |
@@ -104,7 +105,7 @@ jobs:
104105
- name: Run tests
105106
run: |
106107
export PYTHONPATH=src:tests
107-
python -m pytest tests/
108+
python -m pytest tests/ -n 8 --dist=loadgroup --tb=short -x
108109
109110
test-py39-sqlserver2017:
110111
name: Run tests with Python 3.9 and SQL Server 2017
@@ -147,8 +148,8 @@ jobs:
147148
148149
- name: Install activerecord and testsuite from PyPI
149150
run: |
150-
pip install rhosocial-activerecord==1.0.0.dev29
151-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
151+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
152+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
152153
153154
- name: Create test database
154155
run: |
@@ -193,7 +194,7 @@ jobs:
193194
- name: Run tests
194195
run: |
195196
export PYTHONPATH=src:tests
196-
python -m pytest tests/
197+
python -m pytest tests/ -n 8 --dist=loadgroup --tb=short -x
197198
198199
test-py310-sqlserver2019:
199200
name: Run tests with Python 3.10 and SQL Server 2019
@@ -236,8 +237,8 @@ jobs:
236237
237238
- name: Install activerecord and testsuite from PyPI
238239
run: |
239-
pip install rhosocial-activerecord==1.0.0.dev29
240-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
240+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
241+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
241242
242243
- name: Create test database
243244
run: |
@@ -282,7 +283,7 @@ jobs:
282283
- name: Run tests
283284
run: |
284285
export PYTHONPATH=src:tests
285-
python -m pytest tests/
286+
python -m pytest tests/ -n 8 --dist=loadgroup --tb=short -x
286287
287288
test-py311-sqlserver2019:
288289
name: Run tests with Python 3.11 and SQL Server 2019
@@ -325,8 +326,8 @@ jobs:
325326
326327
- name: Install activerecord and testsuite from PyPI
327328
run: |
328-
pip install rhosocial-activerecord==1.0.0.dev29
329-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
329+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
330+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
330331
331332
- name: Create test database
332333
run: |
@@ -371,7 +372,7 @@ jobs:
371372
- name: Run tests
372373
run: |
373374
export PYTHONPATH=src:tests
374-
python -m pytest tests/
375+
python -m pytest tests/ -n 8 --dist=loadgroup --tb=short -x
375376
376377
test-py312-sqlserver2025:
377378
name: Run tests with Python 3.12 and SQL Server 2025
@@ -414,8 +415,8 @@ jobs:
414415
415416
- name: Install activerecord and testsuite from PyPI
416417
run: |
417-
pip install rhosocial-activerecord==1.0.0.dev29
418-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
418+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
419+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
419420
420421
- name: Create test database
421422
run: |
@@ -460,7 +461,7 @@ jobs:
460461
- name: Run tests
461462
run: |
462463
export PYTHONPATH=src:tests
463-
python -m pytest tests/
464+
python -m pytest tests/ -n 8 --dist=loadgroup --tb=short -x
464465
465466
test-py313-sqlserver2022:
466467
name: Run tests with Python 3.13 and SQL Server 2022
@@ -503,8 +504,8 @@ jobs:
503504
504505
- name: Install activerecord and testsuite from PyPI
505506
run: |
506-
pip install rhosocial-activerecord==1.0.0.dev29
507-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
507+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
508+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
508509
509510
- name: Create test database
510511
run: |
@@ -549,7 +550,7 @@ jobs:
549550
- name: Run tests
550551
run: |
551552
export PYTHONPATH=src:tests
552-
python -m pytest tests/
553+
python -m pytest tests/ -n 8 --dist=loadgroup --tb=short -x
553554
554555
test-py313t-sqlserver2019:
555556
name: Run tests with Python 3.13t (free-threaded) and SQL Server 2019
@@ -593,8 +594,8 @@ jobs:
593594
594595
- name: Install activerecord and testsuite from PyPI
595596
run: |
596-
pip install rhosocial-activerecord==1.0.0.dev29
597-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
597+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
598+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
598599
599600
- name: Create test database
600601
run: |
@@ -639,7 +640,7 @@ jobs:
639640
- name: Run tests
640641
run: |
641642
export PYTHONPATH=src:tests
642-
python -m pytest tests/
643+
python -m pytest tests/ -n 8 --dist=loadgroup --tb=short -x
643644
644645
test-py314-sqlserver2025:
645646
name: Run tests with Python 3.14 and SQL Server 2025
@@ -682,8 +683,8 @@ jobs:
682683
683684
- name: Install activerecord and testsuite from PyPI
684685
run: |
685-
pip install rhosocial-activerecord==1.0.0.dev29
686-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
686+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
687+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
687688
688689
- name: Create test database
689690
run: |
@@ -728,9 +729,7 @@ jobs:
728729
- name: Run tests with coverage
729730
run: |
730731
export PYTHONPATH=src:tests
731-
pip install coverage
732-
coverage run --source=src -m pytest tests/ --tb=short
733-
coverage xml
732+
python -m pytest tests/ --tb=short -n 8 --dist=loadgroup --cov=src/rhosocial --cov-branch --cov-report=xml
734733
735734
- name: Upload coverage to Codecov
736735
uses: codecov/codecov-action@v5
@@ -739,7 +738,7 @@ jobs:
739738
files: ./coverage.xml
740739
flags: unittests,python-3.14,sqlserver-2025
741740
name: codecov-sqlserver-3.14-2025
742-
fail_ci_if_error: false
741+
fail_ci_if_error: true
743742

744743
test-py314t-sqlserver2022:
745744
name: Run tests with Python 3.14t (free-threaded) and SQL Server 2022
@@ -782,8 +781,8 @@ jobs:
782781
783782
- name: Install activerecord and testsuite from PyPI
784783
run: |
785-
pip install rhosocial-activerecord==1.0.0.dev29
786-
pip install rhosocial-activerecord-testsuite==1.0.0.dev16
784+
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
785+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
787786
788787
- name: Create test database
789788
run: |
@@ -828,4 +827,4 @@ jobs:
828827
- name: Run tests
829828
run: |
830829
export PYTHONPATH=src:tests
831-
python -m pytest tests/
830+
python -m pytest tests/ -n 8 --dist=loadgroup --tb=short -x

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ namespaces = true
8282

8383
[tool.pytest.ini_options]
8484
minversion = "7.0"
85-
addopts = "-ra -q"
85+
addopts = "-ra -q -p rhosocial.activerecord.testsuite.conftest"
8686
asyncio_mode = "auto"
8787
testpaths = [
8888
"tests",

‎pytest.ini‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ testpaths = tests
55
python_files = test_*.py
66
python_classes = Test*
77
python_functions = test_*
8-
addopts = -v --tb=short
8+
addopts = -v --tb=short -p rhosocial.activerecord.testsuite.conftest
99
filterwarnings =
1010
ignore::DeprecationWarning
1111
markers =

‎requirements-dev.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ pytest
44
pytest-asyncio
55
pytest-cov
66
pytest-benchmark
7+
pytest-xdist
78
coverage>=7.0.0

‎src/rhosocial/activerecord/backend/impl/sqlserver/__main__.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@ def _build_parser():
2222
return parser
2323

2424

25-
def parse_args():
25+
def parse_args(argv=None):
2626
"""Parse command-line arguments.
2727
2828
Provided for test compatibility. In production, main() is used instead.
2929
"""
3030
parser = _build_parser()
31-
return parser.parse_args()
31+
return parser.parse_args(argv)
3232

3333

34-
def main():
34+
def main(argv=None):
3535
parser = _build_parser()
36-
args = parser.parse_args()
36+
args = parser.parse_args(argv)
3737

3838
if args.command is None:
3939
cmd_list = ", ".join(f"'{c}'" for c in COMMAND_NAMES[:-1])

‎src/rhosocial/activerecord/backend/impl/sqlserver/adapters.py‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import json
1010
import uuid
11+
import xml.etree.ElementTree as ET
1112
from abc import ABC, abstractmethod
1213
from datetime import date, datetime, time, timezone
1314
from typing import Any, Dict, List, Type, Union
@@ -203,19 +204,21 @@ def supported_types(self) -> Dict[Type, List[str]]:
203204
def to_database(self, value: Any, target_type: Type = None, options: Dict = None) -> Any:
204205
if value is None:
205206
return None
206-
if isinstance(value, date):
207-
return value
207+
# datetime is an isinstance of date, so it must be checked first
208+
# to truncate to the date component.
208209
if isinstance(value, datetime):
209210
return value.date()
211+
if isinstance(value, date):
212+
return value
210213
raise TypeError(f"Expected date, got {type(value)}")
211214

212215
def from_database(self, value: Any, target_type: Type = None, options: Dict = None) -> date:
213216
if value is None:
214217
return None
215-
if isinstance(value, date):
216-
return value
217218
if isinstance(value, datetime):
218219
return value.date()
220+
if isinstance(value, date):
221+
return value
219222
if isinstance(value, str):
220223
return date.fromisoformat(value)
221224
raise TypeError(f"Cannot convert {type(value)} to date")
@@ -335,8 +338,6 @@ def from_database(self, value: Any, target_type: Type = None, options: Dict = No
335338
raise TypeError(f"Cannot convert {type(value)} to XML")
336339

337340
def _dict_to_xml(self, data: dict, root: str = "root") -> str:
338-
import xml.etree.ElementTree as ET
339-
340341
root_elem = ET.Element(root)
341342
self._build_xml(root_elem, data)
342343
return ET.tostring(root_elem, encoding='unicode')
@@ -411,14 +412,14 @@ def _dict_to_wkt(self, data: dict) -> str:
411412

412413
elif geom_type == 'LineString':
413414
coords = data.get('coordinates', [])
414-
points = " ".join(f"{c[0]} {c[1]}" for c in coords)
415+
points = ", ".join(f"{c[0]} {c[1]}" for c in coords)
415416
return f"LINESTRING({points})"
416417

417418
elif geom_type == 'Polygon':
418419
coords = data.get('coordinates', [[]])
419420
rings = []
420421
for ring in coords:
421-
points = " ".join(f"{c[0]} {c[1]}" for c in ring)
422+
points = ", ".join(f"{c[0]} {c[1]}" for c in ring)
422423
rings.append(f"({points})")
423424
return f"POLYGON({','.join(rings)})"
424425

0 commit comments

Comments
 (0)