rowquery 0.1.2__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {rowquery-0.1.2 → rowquery-0.2.0}/.github/ISSUE_TEMPLATE/config.yml +1 -1
  2. rowquery-0.2.0/.github/workflows/test.yml +115 -0
  3. rowquery-0.2.0/CHANGELOG.md +85 -0
  4. {rowquery-0.1.2 → rowquery-0.2.0}/CLAUDE.md +9 -3
  5. {rowquery-0.1.2 → rowquery-0.2.0}/CONTRIBUTING.md +1 -1
  6. {rowquery-0.1.2 → rowquery-0.2.0}/PKG-INFO +13 -9
  7. {rowquery-0.1.2 → rowquery-0.2.0}/README.md +6 -2
  8. {rowquery-0.1.2 → rowquery-0.2.0}/llms-full.txt +88 -31
  9. {rowquery-0.1.2 → rowquery-0.2.0}/llms.txt +1 -1
  10. {rowquery-0.1.2 → rowquery-0.2.0}/pyproject.toml +6 -6
  11. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/__init__.py +6 -0
  12. rowquery-0.2.0/row_query/adapters/mysql.py +109 -0
  13. rowquery-0.2.0/row_query/adapters/oracle.py +123 -0
  14. rowquery-0.2.0/row_query/adapters/postgresql.py +91 -0
  15. rowquery-0.2.0/row_query/adapters/protocol.py +106 -0
  16. rowquery-0.2.0/row_query/adapters/sqlite.py +85 -0
  17. rowquery-0.2.0/row_query/core/connection.py +220 -0
  18. rowquery-0.2.0/row_query/core/engine.py +128 -0
  19. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/core/exceptions.py +33 -3
  20. rowquery-0.2.0/row_query/core/execution.py +340 -0
  21. rowquery-0.2.0/row_query/core/lexer.py +183 -0
  22. rowquery-0.2.0/row_query/core/migration.py +198 -0
  23. rowquery-0.2.0/row_query/core/params.py +234 -0
  24. rowquery-0.2.0/row_query/core/pool.py +271 -0
  25. rowquery-0.2.0/row_query/core/sanitizer.py +206 -0
  26. rowquery-0.2.0/row_query/core/transaction.py +192 -0
  27. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/aggregate.py +90 -28
  28. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/builder.py +65 -25
  29. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/plan.py +5 -0
  30. {rowquery-0.1.2 → rowquery-0.2.0}/tests/contract/test_adapter_protocol.py +6 -20
  31. rowquery-0.2.0/tests/integration/test_server_backends.py +236 -0
  32. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_builder.py +0 -15
  33. rowquery-0.2.0/tests/unit/test_engine_regressions.py +164 -0
  34. rowquery-0.2.0/tests/unit/test_nested_collections.py +314 -0
  35. rowquery-0.2.0/tests/unit/test_pool.py +184 -0
  36. rowquery-0.2.0/tests/unit/test_review_regressions.py +390 -0
  37. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_sanitizer.py +3 -5
  38. rowquery-0.1.2/.github/workflows/test.yml +0 -48
  39. rowquery-0.1.2/CHANGELOG.md +0 -48
  40. rowquery-0.1.2/row_query/adapters/mysql.py +0 -111
  41. rowquery-0.1.2/row_query/adapters/oracle.py +0 -120
  42. rowquery-0.1.2/row_query/adapters/postgresql.py +0 -104
  43. rowquery-0.1.2/row_query/adapters/protocol.py +0 -81
  44. rowquery-0.1.2/row_query/adapters/sqlite.py +0 -98
  45. rowquery-0.1.2/row_query/core/connection.py +0 -132
  46. rowquery-0.1.2/row_query/core/engine.py +0 -419
  47. rowquery-0.1.2/row_query/core/migration.py +0 -148
  48. rowquery-0.1.2/row_query/core/params.py +0 -122
  49. rowquery-0.1.2/row_query/core/sanitizer.py +0 -233
  50. rowquery-0.1.2/row_query/core/transaction.py +0 -334
  51. {rowquery-0.1.2 → rowquery-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  52. {rowquery-0.1.2 → rowquery-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  53. {rowquery-0.1.2 → rowquery-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  54. {rowquery-0.1.2 → rowquery-0.2.0}/.github/dependabot.yml +0 -0
  55. {rowquery-0.1.2 → rowquery-0.2.0}/.github/workflows/quality.yml +0 -0
  56. {rowquery-0.1.2 → rowquery-0.2.0}/.github/workflows/release.yml +0 -0
  57. {rowquery-0.1.2 → rowquery-0.2.0}/.gitignore +0 -0
  58. {rowquery-0.1.2 → rowquery-0.2.0}/.pre-commit-config.yaml +0 -0
  59. {rowquery-0.1.2 → rowquery-0.2.0}/CODE_OF_CONDUCT.md +0 -0
  60. {rowquery-0.1.2 → rowquery-0.2.0}/LICENSE +0 -0
  61. {rowquery-0.1.2 → rowquery-0.2.0}/examples/01_basic_query.py +0 -0
  62. {rowquery-0.1.2 → rowquery-0.2.0}/examples/02_model_mapping.py +0 -0
  63. {rowquery-0.1.2 → rowquery-0.2.0}/examples/03_aggregate.py +0 -0
  64. {rowquery-0.1.2 → rowquery-0.2.0}/examples/04_transactions.py +0 -0
  65. {rowquery-0.1.2 → rowquery-0.2.0}/examples/05_async.py +0 -0
  66. {rowquery-0.1.2 → rowquery-0.2.0}/examples/06_migrations.py +0 -0
  67. {rowquery-0.1.2 → rowquery-0.2.0}/examples/07_repository.py +0 -0
  68. {rowquery-0.1.2 → rowquery-0.2.0}/examples/README.md +0 -0
  69. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/adapters/__init__.py +0 -0
  70. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/core/__init__.py +0 -0
  71. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/core/enums.py +0 -0
  72. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/core/registry.py +0 -0
  73. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/__init__.py +0 -0
  74. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/model.py +0 -0
  75. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/protocol.py +0 -0
  76. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/repository/__init__.py +0 -0
  77. {rowquery-0.1.2 → rowquery-0.2.0}/row_query/repository/base.py +0 -0
  78. {rowquery-0.1.2 → rowquery-0.2.0}/tests/__init__.py +0 -0
  79. {rowquery-0.1.2 → rowquery-0.2.0}/tests/conftest.py +0 -0
  80. {rowquery-0.1.2 → rowquery-0.2.0}/tests/contract/__init__.py +0 -0
  81. {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/billing/invoice/list.sql +0 -0
  82. {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/user/count.sql +0 -0
  83. {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/user/get_by_id.sql +0 -0
  84. {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/user/insert.sql +0 -0
  85. {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/user/list.sql +0 -0
  86. {rowquery-0.1.2 → rowquery-0.2.0}/tests/integration/__init__.py +0 -0
  87. {rowquery-0.1.2 → rowquery-0.2.0}/tests/integration/test_sqlite.py +0 -0
  88. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/__init__.py +0 -0
  89. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_aggregate_mapper.py +0 -0
  90. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_engine.py +0 -0
  91. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_migration.py +0 -0
  92. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_model_mapper.py +0 -0
  93. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_params.py +0 -0
  94. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_registry.py +0 -0
  95. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_repository.py +0 -0
  96. {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_transaction.py +0 -0
@@ -1,5 +1,5 @@
1
1
  blank_issues_enabled: false
2
2
  contact_links:
3
3
  - name: Question or Discussion
4
- url: https://github.com/maksim-shevtsov/RowQuery/discussions
4
+ url: https://github.com/MaksimShevtsov/RowQuery/discussions
5
5
  about: Ask questions or discuss ideas
@@ -0,0 +1,115 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main, "001-*"]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ test:
12
+ name: Test (Python ${{ matrix.python-version }})
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
18
+
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v5
24
+ with:
25
+ enable-cache: true
26
+
27
+ - name: Set up Python ${{ matrix.python-version }}
28
+ uses: actions/setup-python@v6
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+
32
+ - name: Install dependencies
33
+ run: uv sync --extra sqlite --extra dev
34
+
35
+ - name: Run unit and contract tests
36
+ run: uv run pytest tests/unit/ tests/contract/ -v --cov=row_query --cov-report=xml --cov-report=term
37
+
38
+ - name: Run integration tests
39
+ run: uv run pytest tests/integration/ -m integration -v
40
+
41
+ - name: Upload coverage to Codecov
42
+ if: matrix.python-version == '3.12'
43
+ uses: codecov/codecov-action@v4
44
+ with:
45
+ file: ./coverage.xml
46
+ flags: unittests
47
+ name: codecov-umbrella
48
+ fail_ci_if_error: false
49
+
50
+ integration-servers:
51
+ name: Integration (PostgreSQL + MySQL + Oracle)
52
+ runs-on: ubuntu-latest
53
+
54
+ services:
55
+ postgres:
56
+ image: postgres:16-alpine
57
+ env:
58
+ POSTGRES_PASSWORD: rowquery
59
+ POSTGRES_DB: rowquery
60
+ ports:
61
+ - 5432:5432
62
+ options: >-
63
+ --health-cmd "pg_isready -U postgres"
64
+ --health-interval 5s
65
+ --health-timeout 5s
66
+ --health-retries 10
67
+ mysql:
68
+ image: mysql:8.4
69
+ env:
70
+ MYSQL_ROOT_PASSWORD: rowquery
71
+ MYSQL_DATABASE: rowquery
72
+ ports:
73
+ - 3306:3306
74
+ options: >-
75
+ --health-cmd "mysqladmin ping -prowquery --silent"
76
+ --health-interval 5s
77
+ --health-timeout 5s
78
+ --health-retries 20
79
+ oracle:
80
+ image: gvenzl/oracle-free:23-slim-faststart
81
+ env:
82
+ ORACLE_PASSWORD: rowquery
83
+ APP_USER: rowquery
84
+ APP_USER_PASSWORD: rowquery
85
+ ports:
86
+ - 1521:1521
87
+ options: >-
88
+ --health-cmd healthcheck.sh
89
+ --health-interval 10s
90
+ --health-timeout 5s
91
+ --health-retries 30
92
+
93
+ env:
94
+ ROWQUERY_TEST_POSTGRESQL: postgresql://postgres:rowquery@127.0.0.1:5432/rowquery
95
+ ROWQUERY_TEST_MYSQL: mysql://root:rowquery@127.0.0.1:3306/rowquery
96
+ ROWQUERY_TEST_ORACLE: oracle://rowquery:rowquery@127.0.0.1:1521/FREEPDB1
97
+
98
+ steps:
99
+ - uses: actions/checkout@v6
100
+
101
+ - name: Install uv
102
+ uses: astral-sh/setup-uv@v5
103
+ with:
104
+ enable-cache: true
105
+
106
+ - name: Set up Python
107
+ uses: actions/setup-python@v6
108
+ with:
109
+ python-version: "3.12"
110
+
111
+ - name: Install dependencies
112
+ run: uv sync --extra all --extra dev
113
+
114
+ - name: Run server integration tests
115
+ run: uv run pytest tests/integration/test_server_backends.py -m integration -v -rs
@@ -0,0 +1,85 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-10-21
11
+
12
+ ### Fixed
13
+ - **Transaction state no longer leaks between calls.** Connections are rolled back when they return to the pool, so reads no longer leave PostgreSQL sessions `idle in transaction`, MySQL `REPEATABLE READ` readers no longer see a stale snapshot, and a failed statement no longer leaves a PostgreSQL connection in the `current transaction is aborted` state.
14
+ - **Real connection pooling.** The pool now waits for a free connection (up to `pool_timeout`) instead of raising `RuntimeError` when exhausted, opens connections lazily, honours `pool_recycle`, and discards connections that fail the reset or are reported broken. `Engine` can be used from several threads (SQLite connections are opened with `check_same_thread=False`), and `asyncio.gather` with more tasks than `pool_size` works.
15
+ - **`%` in SQL for PostgreSQL/MySQL.** Literal `%` (e.g. `LIKE 'a%'`) is escaped when `:name` placeholders are converted to `%(name)s`.
16
+ - **Migrations**: files may contain several statements; versions are ordered numerically (`9_x` before `10_y`); the tracking table and inserts work on PostgreSQL, MySQL and Oracle; duplicate version numbers are rejected.
17
+ - **SQLSanitizer bypasses**: constructs whose extent differs between MySQL and PostgreSQL are rejected (backslashes inside any quoted literal or identifier, dollar-quoted strings, `#` outside literals, `--` not followed by whitespace, `
18
+ ` inside `--` comments); the `allowed_verbs` check skips leading comments and rejects SQL whose verb cannot be determined (fail-closed); MySQL `/*! ... */` executable comments are rejected when comments are not stripped; unterminated block comments are rejected.
19
+ - `Engine.transaction()` acquires its connection on `__enter__` instead of at call time.
20
+ - Transactions now have the same query API as engines: `fetch_one` raises `MultipleRowsError`, plus `mapper=` and `fetch_scalar`.
21
+ - `fetch_one` reads at most two rows instead of the whole result.
22
+ - MySQL cursors are buffered and always closed, avoiding `Unread result found` after `fetch_scalar`/`fetch_one`.
23
+ - PostgreSQL connection strings are built with `psycopg.conninfo.make_conninfo` (passwords with spaces or quotes work); `ConnectionConfig.extra` is passed to every driver.
24
+ - Oracle: DSN defaults to port 1521 when `port` is unset; only ALL-CAPS column names are lowercased, so quoted mixed-case aliases are preserved.
25
+ - Single-word inline SQL (`VACUUM`, `COMMIT`) is no longer mistaken for a registry key.
26
+ - SQLite transactions and migrations now cover DDL: an explicit `BEGIN` is issued, so a failing transaction or migration rolls back `CREATE`/`ALTER`/`DROP` too (the `sqlite3` module only opens transactions implicitly before DML).
27
+ - A connection released after `close_pool()` is closed instead of being handed to the new pool; pool initialisation is thread-safe.
28
+ - MySQL connection liveness is checked locally instead of pinging the server on every acquire; pool health checks run outside the pool lock.
29
+ - Applied migrations are matched by version number, so renaming `9_x.sql` to `009_x.sql` does not re-apply it.
30
+ - Parameter conversion honours the backend's string-literal rules (`'C:\'` is a complete literal on PostgreSQL).
31
+ - Strict aggregate mapping also validates reference and value-object columns.
32
+ - Project URLs in `pyproject.toml` point to the correct GitHub repository.
33
+
34
+ ### Added
35
+ - **Nested collections** in aggregate mappings: `.collection("lines.discounts", Discount, prefix="disc__", key="id")` nests a collection inside a previously declared one, at any depth. Children are deduplicated per parent instance; strict mode validates nested columns, prefixes and identity key columns (root, collections at every level, references) and reports collections by their full path. Dotted paths for `reference()` / `value_object()` are rejected at `build()`. `CollectionPlan.children` is now populated and used by `AggregateMapper`.
36
+ - Portable positional placeholders: `?` is converted to `%s` (PostgreSQL, MySQL) or `:1, :2, ...` (Oracle); `??` is a literal `?` (e.g. the PostgreSQL JSONB operator). SQL already using native `%s` / `%(name)s` placeholders is passed through unchanged.
37
+ - `QueryExecutionError` (database rejected the query) and `IntegrityError` (constraint violation), both carrying the driver exception as `__cause__`; `PoolTimeoutError`.
38
+ - `Engine.close()` / `AsyncEngine.close()`; `ConnectionManager.acquire()` / `release()`.
39
+ - CI runs the integration suite against PostgreSQL, MySQL and Oracle (23ai Free) service containers.
40
+
41
+ ### Changed
42
+ - **Breaking:** errors that are not binding problems now raise `QueryExecutionError` instead of `ParameterBindingError` (which is now a subclass of `QueryExecutionError`). Catch `ExecutionError` or `QueryExecutionError` to handle all database errors.
43
+ - **Breaking (adapter protocol):** adapters now implement `connect`/`close_connection`/`is_connection_alive`/`execute` (async: `connect_async`/`close_connection_async`/`execute_async`) plus `begin`/`begin_async`, `positional_paramstyle` and `backslash_escapes`; pooling moved into `row_query.core.pool`. `TransactionManager`/`AsyncTransactionManager` take a connection manager instead of a raw connection.
44
+ - `ConnectionConfig.pool_timeout` and `pool_recycle` are floats and are now enforced.
45
+
46
+ ## [0.1.2] - 2026-02-22
47
+
48
+ ### Added
49
+ - **Inline SQL support**: All engine and transaction methods (`fetch_one`, `fetch_all`, `fetch_scalar`, `execute`) now accept raw SQL strings in addition to registry keys. A string containing whitespace is treated as inline SQL; a dot-separated identifier like `user.get_by_id` is resolved from the registry.
50
+ - **Flexible parameter binding**: `params` argument now accepts `dict` (named), `tuple`/`list` (positional), or a single scalar value (automatically wrapped in a tuple). Previously only `dict | None` was accepted.
51
+ - **`SQLSanitizer`** — configurable sanitizer applied to inline SQL before execution:
52
+ - `strip_comments` (default `True`): removes `--` line comments and `/* */` block comments while preserving string literals.
53
+ - `block_multiple_statements` (default `True`): rejects SQL containing a statement-terminating `;` followed by additional content (prevents query stacking attacks).
54
+ - `allowed_verbs` (default `None`): restricts the leading SQL keyword to a caller-supplied `frozenset` (e.g. `frozenset({"SELECT"})`). Registry queries are never sanitized.
55
+ - **`SQLSanitizationError`** exception (subclass of `ExecutionError`) raised when a sanitization check fails.
56
+ - **`is_raw_sql()`** and **`coerce_params()`** helpers exported from `row_query.core.params`.
57
+ - 65 new unit tests covering all sanitizer behaviour (`tests/unit/test_sanitizer.py`).
58
+
59
+ ## [0.1.0] - 2025-02-16
60
+
61
+ ### Added
62
+ - Initial alpha release
63
+ - Core query execution engine (`Engine`, `AsyncEngine`)
64
+ - Connection management with pooling support (`ConnectionManager`, `AsyncConnectionManager`)
65
+ - Multi-database adapter pattern supporting SQLite, PostgreSQL, MySQL, and Oracle
66
+ - SQL registry for loading `.sql` files into dot-separated namespaces (`SQLRegistry`)
67
+ - Parameter normalization supporting `:name` syntax across all database backends
68
+ - Model mapping for dataclasses, Pydantic models, and plain classes (`ModelMapper`)
69
+ - Aggregate mapping for complex object graph reconstruction with single-pass O(n) algorithm
70
+ - Collection mapping for one-to-many relationships in aggregate patterns
71
+ - Value object mapping for embedded entities in aggregates
72
+ - Reference mapping for foreign key relationships
73
+ - Transaction management with context manager support (`TransactionManager`, `AsyncTransactionManager`)
74
+ - Migration management system with version tracking (`MigrationManager`, `MigrationInfo`)
75
+ - Repository pattern base classes for DDD-style code organization (`Repository`, `AsyncRepository`)
76
+ - Comprehensive exception hierarchy with 18 custom exception classes
77
+ - Full type safety with mypy strict mode compliance
78
+ - Support for Python 3.10, 3.11, 3.12, and 3.13
79
+ - 125 unit, contract, and integration tests
80
+
81
+ [Unreleased]: https://github.com/MaksimShevtsov/RowQuery/compare/v0.2.0...HEAD
82
+ [0.2.0]: https://github.com/MaksimShevtsov/RowQuery/releases/tag/v0.2.0
83
+ [0.1.2]: https://github.com/MaksimShevtsov/RowQuery/releases/tag/v0.1.2
84
+ [0.1.1]: https://github.com/MaksimShevtsov/RowQuery/releases/tag/v0.1.1
85
+ [0.1.0]: https://github.com/MaksimShevtsov/RowQuery/releases/tag/v0.1.0
@@ -19,7 +19,9 @@ uv sync --extra all # Install all database drivers
19
19
  ## Commands
20
20
 
21
21
  ```bash
22
- uv run pytest # Run all tests (125 tests)
22
+ uv run pytest # Run all tests
23
+ # Server integration tests run when ROWQUERY_TEST_POSTGRESQL / ROWQUERY_TEST_MYSQL
24
+ # are set to postgresql://user:pw@host:port/db and mysql://user:pw@host:port/db URLs
23
25
  uv run pytest tests/unit/ # Unit tests only
24
26
  uv run pytest tests/contract/ # Protocol contract tests
25
27
  uv run pytest tests/integration/ # SQLite integration tests
@@ -32,13 +34,17 @@ uv run mypy row_query/ # Type check (strict mode)
32
34
 
33
35
  - **`row_query/core/`** — Core engine
34
36
  - `connection.py` — `ConnectionConfig` (Pydantic), `ConnectionManager`, `AsyncConnectionManager`
37
+ - `pool.py` — `SyncPool`/`AsyncPool`: bounded, lazy, timeout + recycle, rollback on release
38
+ - `execution.py` — `SyncExecutor`/`AsyncExecutor` (query API shared by engines and transactions), driver error translation
35
39
  - `engine.py` — `Engine` and `AsyncEngine` (fetch_one, fetch_all, fetch_scalar, execute, transaction)
36
40
  - `registry.py` — `SQLRegistry` (loads .sql files into dot-separated namespaces)
37
41
  - `transaction.py` — `TransactionManager`, `AsyncTransactionManager`
38
42
  - `migration.py` — `MigrationManager`, `MigrationInfo`
39
- - `params.py` — `:name` → `%(name)s` parameter normalizer with cache
43
+ - `params.py` — `:name`/`?` → driver placeholder conversion (escapes `%` for pyformat)
44
+ - `lexer.py` — SQL tokenizer (strings, `$$`, `E''`, identifiers, comments) and statement splitter
45
+ - `sanitizer.py` — fail-closed `SQLSanitizer` for inline SQL
40
46
  - `enums.py` — `DatabaseBackend` enum
41
- - `exceptions.py` — Full exception hierarchy (18 classes)
47
+ - `exceptions.py` — Full exception hierarchy
42
48
 
43
49
  - **`row_query/adapters/`** — Database drivers
44
50
  - `protocol.py` — `SyncAdapter`, `AsyncAdapter` runtime-checkable protocols
@@ -13,7 +13,7 @@ Thank you for your interest in contributing! This document provides guidelines f
13
13
 
14
14
  ```bash
15
15
  # Clone repository
16
- git clone https://github.com/maksim-shevtsov/RowQuery.git
16
+ git clone https://github.com/MaksimShevtsov/RowQuery.git
17
17
  cd RowQuery
18
18
 
19
19
  # Install all dependencies (including all database drivers)
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: rowquery
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: A SQL-first Python library for querying and mapping data across multiple database backends.
5
- Project-URL: Homepage, https://github.com/maksim-shevtsov/RowQuery
6
- Project-URL: Documentation, https://github.com/maksim-shevtsov/RowQuery#readme
7
- Project-URL: Repository, https://github.com/maksim-shevtsov/RowQuery.git
8
- Project-URL: Issues, https://github.com/maksim-shevtsov/RowQuery/issues
9
- Project-URL: Changelog, https://github.com/maksim-shevtsov/RowQuery/blob/main/CHANGELOG.md
5
+ Project-URL: Homepage, https://github.com/MaksimShevtsov/RowQuery
6
+ Project-URL: Documentation, https://github.com/MaksimShevtsov/RowQuery#readme
7
+ Project-URL: Repository, https://github.com/MaksimShevtsov/RowQuery.git
8
+ Project-URL: Issues, https://github.com/MaksimShevtsov/RowQuery/issues
9
+ Project-URL: Changelog, https://github.com/MaksimShevtsov/RowQuery/blob/main/CHANGELOG.md
10
10
  Author-email: Maksim Shevtsov <maksim.shautsou.dev@gmail.com>
11
11
  License: MIT
12
12
  License-File: LICENSE
@@ -52,8 +52,8 @@ Description-Content-Type: text/markdown
52
52
  [![PyPI version](https://badge.fury.io/py/rowquery.svg)](https://pypi.org/project/rowquery/)
53
53
  [![Python Versions](https://img.shields.io/pypi/pyversions/rowquery.svg)](https://pypi.org/project/rowquery/)
54
54
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
55
- [![Tests](https://github.com/maksim-shevtsov/RowQuery/workflows/Tests/badge.svg)](https://github.com/maksim-shevtsov/RowQuery/actions)
56
- [![codecov](https://codecov.io/gh/maksim-shevtsov/RowQuery/branch/main/graph/badge.svg)](https://codecov.io/gh/maksim-shevtsov/RowQuery)
55
+ [![Tests](https://github.com/MaksimShevtsov/RowQuery/actions/workflows/test.yml/badge.svg)](https://github.com/MaksimShevtsov/RowQuery/actions/workflows/test.yml)
56
+ [![codecov](https://codecov.io/gh/MaksimShevtsov/RowQuery/branch/main/graph/badge.svg)](https://codecov.io/gh/MaksimShevtsov/RowQuery)
57
57
  [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
58
58
 
59
59
  A SQL-first Python library for querying and mapping data across multiple database backends.
@@ -119,6 +119,10 @@ count = engine.fetch_scalar("SELECT COUNT(*) FROM users")
119
119
  rows = engine.fetch_all("SELECT * FROM users WHERE id IN (?, ?)", [1, 2])
120
120
  ```
121
121
 
122
+ Placeholders are portable: use `:name` with a dict or `?` with positional values on any
123
+ backend. RowQuery converts them to the driver's native style (`%(name)s` / `%s` for
124
+ PostgreSQL and MySQL, `:1` for Oracle) and escapes literal `%` such as `LIKE 'a%'`.
125
+
122
126
  ### 3. Map to Models
123
127
  ```python
124
128
  from dataclasses import dataclass
@@ -3,8 +3,8 @@
3
3
  [![PyPI version](https://badge.fury.io/py/rowquery.svg)](https://pypi.org/project/rowquery/)
4
4
  [![Python Versions](https://img.shields.io/pypi/pyversions/rowquery.svg)](https://pypi.org/project/rowquery/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![Tests](https://github.com/maksim-shevtsov/RowQuery/workflows/Tests/badge.svg)](https://github.com/maksim-shevtsov/RowQuery/actions)
7
- [![codecov](https://codecov.io/gh/maksim-shevtsov/RowQuery/branch/main/graph/badge.svg)](https://codecov.io/gh/maksim-shevtsov/RowQuery)
6
+ [![Tests](https://github.com/MaksimShevtsov/RowQuery/actions/workflows/test.yml/badge.svg)](https://github.com/MaksimShevtsov/RowQuery/actions/workflows/test.yml)
7
+ [![codecov](https://codecov.io/gh/MaksimShevtsov/RowQuery/branch/main/graph/badge.svg)](https://codecov.io/gh/MaksimShevtsov/RowQuery)
8
8
  [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
9
9
 
10
10
  A SQL-first Python library for querying and mapping data across multiple database backends.
@@ -70,6 +70,10 @@ count = engine.fetch_scalar("SELECT COUNT(*) FROM users")
70
70
  rows = engine.fetch_all("SELECT * FROM users WHERE id IN (?, ?)", [1, 2])
71
71
  ```
72
72
 
73
+ Placeholders are portable: use `:name` with a dict or `?` with positional values on any
74
+ backend. RowQuery converts them to the driver's native style (`%(name)s` / `%s` for
75
+ PostgreSQL and MySQL, `:1` for Oracle) and escapes literal `%` such as `LIKE 'a%'`.
76
+
73
77
  ### 3. Map to Models
74
78
  ```python
75
79
  from dataclasses import dataclass
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What Is RowQuery
6
6
 
7
- RowQuery is a Python library for executing raw SQL queries against multiple database backends with structured result mapping. It is currently in alpha (v0.1.2) and requires Python >=3.10.
7
+ RowQuery is a Python library for executing raw SQL queries against multiple database backends with structured result mapping. It is currently in alpha (v0.2.0) and requires Python >=3.10.
8
8
 
9
9
  Key features:
10
10
 
@@ -22,7 +22,7 @@ RowQuery's core dependency is Pydantic >=2.0.0. Database-specific drivers are op
22
22
  - **Not an ORM.** RowQuery does not generate SQL from Python model definitions. You write all SQL yourself.
23
23
  - **Not a schema generator.** RowQuery does not create database tables from class definitions. Use migrations or external tools for DDL.
24
24
  - **Not a migration-only tool.** While RowQuery includes a `MigrationManager`, it is a convenience feature, not the library's primary purpose.
25
- - **Not a connection pooling library.** RowQuery delegates pooling to the underlying database drivers (psycopg, aiomysql, oracledb). It manages connection lifecycle but does not implement its own pool.
25
+ - **Not a full-featured connection pooling library.** RowQuery ships a small built-in bounded pool (lazy connections, waiting with `pool_timeout`, recycling after `pool_recycle`, rollback on release). It has no pre-ping, statistics or external pooler integration.
26
26
  - **Not a query builder with DSL syntax.** There is no fluent API for constructing SQL. You pass SQL strings directly or load them from `.sql` files via `SQLRegistry`.
27
27
  - **Not a data validation framework.** RowQuery maps query results to objects but does not validate business rules or enforce constraints beyond what the database itself provides.
28
28
 
@@ -32,12 +32,12 @@ RowQuery's core dependency is Pydantic >=2.0.0. Database-specific drivers are op
32
32
 
33
33
  Install RowQuery with no database-specific drivers:
34
34
 
35
- ```python
35
+ ```bash
36
36
  # Using uv (recommended)
37
- # uv add row-query
37
+ # uv add rowquery
38
38
 
39
39
  # Using pip
40
- # pip install row-query
40
+ # pip install rowquery
41
41
  ```
42
42
 
43
43
  The core installation includes only the `pydantic` dependency and the SQLite sync adapter (which uses Python's built-in `sqlite3` module).
@@ -195,16 +195,20 @@ engine = AsyncEngine.from_config(config, registry)
195
195
  AsyncEngine has identical methods, all `async`.
196
196
 
197
197
  **Query resolution:** The `query` parameter can be either:
198
- - A **registry key** (no whitespace): `"users.get_by_id"` — resolved via SQLRegistry.
199
- - **Inline SQL** (contains whitespace): `"SELECT * FROM users WHERE id = :id"` — used directly.
198
+ - A **registry key**: a registered name or any dotted identifier such as `"users.get_by_id"` — resolved via SQLRegistry (unknown dotted keys raise `QueryNotFoundError`).
199
+ - **Inline SQL**: anything else, e.g. `"SELECT * FROM users WHERE id = :id"` or a single-word statement such as `"VACUUM"` — used directly.
200
200
 
201
201
  **Parameter passing:** The `params` parameter accepts:
202
- - `dict` — Named parameters: `{"id": 1, "name": "Alice"}`
203
- - `tuple` or `list` — Positional parameters: `(1, "Alice")`
202
+ - `dict` — Named parameters, written as `:name` in SQL: `{"id": 1, "name": "Alice"}`
203
+ - `tuple` or `list` — Positional parameters, written as `?` in SQL: `(1, "Alice")`
204
204
  - Scalar value — Wrapped as `(value,)` automatically
205
205
  - `None` — No parameters (default)
206
206
 
207
- RowQuery automatically converts `:name` parameter syntax to the format required by each backend (`%(name)s` for PostgreSQL and MySQL, `:name` for SQLite and Oracle).
207
+ Both placeholder styles are portable: RowQuery converts `:name` to `%(name)s` and `?` to `%s` for PostgreSQL and MySQL, and `?` to `:1, :2, ...` for Oracle. Literal `%` characters (e.g. `LIKE 'a%'`) are escaped automatically. Write `??` for a literal `?` (e.g. the PostgreSQL JSONB `?` operator) when using positional parameters. SQL already written in a driver's native style (`%s` / `%(name)s`) is passed through unchanged.
208
+
209
+ **Connections and transaction state:** each engine call borrows a pooled connection. `execute()` commits immediately; any other open transaction state (read snapshots, locks, a failed statement) is rolled back when the connection returns to the pool. `Engine` can be shared between threads.
210
+
211
+ **Errors:** driver exceptions are translated: constraint violations raise `IntegrityError`, binding problems `ParameterBindingError`, everything else `QueryExecutionError` (all subclasses of `ExecutionError`). The original driver exception is available as `__cause__`.
208
212
 
209
213
  **Optional SQLSanitizer:**
210
214
 
@@ -245,10 +249,10 @@ sqlite_file_config = ConnectionConfig(driver="sqlite", database="/path/to/mydb.s
245
249
  | `user` | `str \| None` | `None` | Authentication username |
246
250
  | `password` | `str \| None` | `None` | Authentication password |
247
251
  | `database` | `str` | (required) | Database name or path (`:memory:` for SQLite in-memory) |
248
- | `pool_size` | `int` | `5` | Maximum connections in pool |
249
- | `pool_timeout` | `int` | `30` | Seconds to wait for a connection from pool |
250
- | `pool_recycle` | `int` | `1800` | Seconds before recycling idle connections |
251
- | `extra` | `dict[str, Any]` | `{}` | Additional driver-specific parameters |
252
+ | `pool_size` | `int` | `5` | Maximum open connections. Connections are opened lazily. |
253
+ | `pool_timeout` | `float` | `30` | Seconds to wait for a free connection before raising `PoolTimeoutError`. |
254
+ | `pool_recycle` | `float` | `1800` | Reopen connections older than this many seconds (`0` disables; ignored for SQLite). |
255
+ | `extra` | `dict[str, Any]` | `{}` | Additional driver-specific connection arguments (passed to the driver's connect call; libpq parameters for PostgreSQL; `dsn` overrides host/port/database for Oracle). |
252
256
 
253
257
  See the Database Backends section for backend-specific ConnectionConfig examples.
254
258
 
@@ -351,7 +355,7 @@ config = ConnectionConfig(driver="sqlite", database="/path/to/mydb.sqlite")
351
355
 
352
356
  **Parameter style:** `:name` (named parameters). SQL uses `:name` syntax directly.
353
357
 
354
- **Adapter details:** Connections use WAL journal mode and `sqlite3.Row` row factory for dict-like access.
358
+ **Adapter details:** Connections use WAL journal mode and `sqlite3.Row` row factory for dict-like access. They are opened with `check_same_thread=False`; the pool guarantees one thread uses a connection at a time. Note that every `:memory:` connection is a separate database, so use `pool_size=1` (or a file) for in-memory databases.
355
359
 
356
360
  ### PostgreSQL
357
361
 
@@ -376,7 +380,7 @@ config = ConnectionConfig(
376
380
 
377
381
  **Parameter style:** `pyformat`. RowQuery automatically converts `:name` in your SQL to `%(name)s` before execution.
378
382
 
379
- **Adapter details:** Uses `psycopg.rows.dict_row` row factory. Connection string is built from ConnectionConfig fields.
383
+ **Adapter details:** Uses `psycopg.rows.dict_row` row factory. The connection string is built with `psycopg.conninfo.make_conninfo`, so special characters in passwords are quoted; `extra` adds libpq parameters such as `sslmode`. On Windows, async mode requires a `SelectorEventLoop`.
380
384
 
381
385
  ### MySQL
382
386
 
@@ -401,7 +405,7 @@ config = ConnectionConfig(
401
405
 
402
406
  **Parameter style:** `pyformat`. RowQuery automatically converts `:name` in your SQL to `%(name)s` before execution.
403
407
 
404
- **Adapter details:** Sync adapter uses `cursor(dictionary=True)`. Async adapter uses `aiomysql.DictCursor`.
408
+ **Adapter details:** Sync adapter uses buffered `cursor(dictionary=True, buffered=True)` cursors. Async adapter uses `aiomysql.DictCursor`. Cursors are always closed after use.
405
409
 
406
410
  ### Oracle
407
411
 
@@ -424,9 +428,9 @@ config = ConnectionConfig(
424
428
  )
425
429
  ```
426
430
 
427
- **Parameter style:** `named`. SQL uses `:name` syntax directly (same as SQLite).
431
+ **Parameter style:** `named`. SQL uses `:name` syntax directly (same as SQLite). Positional `?` placeholders are converted to `:1, :2, ...`.
428
432
 
429
- **Adapter details:** DSN format is `host:port/database`. Uses a custom row factory to convert tuples to dicts with lowercased column names.
433
+ **Adapter details:** DSN format is `host:port/database` (port defaults to 1521; `extra={"dsn": ...}` overrides). A custom row factory converts tuples to dicts. Column names that Oracle reports in ALL CAPS (unquoted identifiers) are lowercased to match the other backends; quoted mixed-case aliases are preserved.
430
434
 
431
435
  ## Result Mapping
432
436
 
@@ -544,12 +548,55 @@ plan = (
544
548
  | `.key(field_name)` | **Required.** Sets the identity key field for the root entity. |
545
549
  | `.auto_fields()` | Auto-maps all fields of the root class (excluding collections, references, value objects). |
546
550
  | `.field(attr_name, column_name=None)` | Explicitly map a single field. `column_name` defaults to `attr_name`. |
547
- | `.collection(name, entity_class, prefix, key)` | Define a child collection (one-to-many). |
551
+ | `.collection(name, entity_class, prefix, key)` | Define a child collection (one-to-many). A dotted `name` such as `"lines.discounts"` nests the collection inside a previously declared collection (any depth). |
548
552
  | `.reference(name, entity_class, prefix, key=None)` | Define a reference (many-to-one). `key` defaults to `"id"`. |
549
553
  | `.value_object(name, vo_class, prefix)` | Define an embedded value object. |
550
554
  | `.strict(enabled=True)` | Enable strict mode: validates all mapped columns exist in result set. |
551
555
  | `.build()` | Compile and return an `AggregatePlan`. Raises `PlanCompilationError` if `.key()` was not called or prefixes collide. |
552
556
 
557
+ **Nested collections:** declare the parent collection first, then the child with a dotted path. The nested attribute (`discounts` below) is excluded from the parent entity's column mapping and initialised to an empty list.
558
+
559
+ ```python
560
+ @dataclass
561
+ class Discount:
562
+ id: int
563
+ percent: int
564
+
565
+ @dataclass
566
+ class Line:
567
+ id: int
568
+ sku: str
569
+ discounts: list[Discount] = field(default_factory=list)
570
+
571
+ @dataclass
572
+ class Invoice:
573
+ id: int
574
+ lines: list[Line] = field(default_factory=list)
575
+
576
+ plan = (
577
+ aggregate(Invoice, prefix="invoice__")
578
+ .key("id")
579
+ .auto_fields()
580
+ .collection("lines", Line, prefix="line__", key="id")
581
+ .collection("lines.discounts", Discount, prefix="disc__", key="id")
582
+ .build()
583
+ )
584
+
585
+ invoices = engine.fetch_all(
586
+ """
587
+ SELECT i.id AS invoice__id,
588
+ l.id AS line__id, l.sku AS line__sku,
589
+ d.id AS disc__id, d.percent AS disc__percent
590
+ FROM invoices i
591
+ LEFT JOIN lines l ON l.invoice_id = i.id
592
+ LEFT JOIN discounts d ON d.line_id = l.id
593
+ """,
594
+ mapper=AggregateMapper(plan),
595
+ )
596
+ ```
597
+
598
+ Nested collections currently support collection members only; references and value objects are declared on the aggregate root.
599
+
553
600
  #### AggregateMapper
554
601
 
555
602
  AggregateMapper consumes the plan and reconstructs objects from flat rows:
@@ -584,11 +631,12 @@ orders = engine.fetch_all(
584
631
 
585
632
  **Behavior:**
586
633
  - Deduplicates root entities by key field (e.g., one Order per unique `order__id`).
587
- - Deduplicates collection children by their key field.
634
+ - Deduplicates collection children by their key field, per parent instance (the same child key under two different parents yields two children).
635
+ - Nested collections are filled recursively in the same single pass; a NULL child key skips that child and everything nested below it.
588
636
  - References and value objects are set once per root (first non-NULL occurrence).
589
637
  - Rows with NULL root key are silently skipped.
590
638
  - `map_one()` raises `NotImplementedError` — use `map_many()` only.
591
- - In strict mode, raises `StrictModeViolation` if columns are missing or unknown prefix groups appear.
639
+ - In strict mode, raises `StrictModeViolation` if mapped or key columns (root, collections at every level, references) are missing, or unknown prefix groups appear.
592
640
 
593
641
  ## Transactions
594
642
 
@@ -626,11 +674,14 @@ with engine.transaction() as tx:
626
674
 
627
675
  **Available methods within a transaction:**
628
676
 
677
+ Transactions have the same query API as the engine (including `mapper=`, `fetch_scalar` and `MultipleRowsError` from `fetch_one`); writes are committed only when the transaction commits. The connection is acquired when the `with` block is entered. On SQLite an explicit `BEGIN` is issued so DDL statements are part of the transaction.
678
+
629
679
  | Method | Signature | Returns |
630
680
  |--------|-----------|---------|
631
681
  | `execute` | `(query, params=None)` | `int` (affected row count) |
632
- | `fetch_one` | `(query, params=None)` | `dict[str, Any] \| None` |
633
- | `fetch_all` | `(query, params=None)` | `list[dict[str, Any]]` |
682
+ | `fetch_one` | `(query, params=None, *, mapper=None)` | `dict`, mapped object, or `None` |
683
+ | `fetch_all` | `(query, params=None, *, mapper=None)` | `list[dict]` or `list[T]` |
684
+ | `fetch_scalar` | `(query, params=None)` | `Any` or `None` |
634
685
  | `commit` | `()` | `None` |
635
686
  | `rollback` | `()` | `None` |
636
687
 
@@ -727,11 +778,11 @@ version = manager.current_version() # "003" or None if no migrations applied
727
778
  | `file_path` | `Path` | Absolute path to the `.sql` file |
728
779
  | `applied` | `bool` | Whether this migration has been applied (default: `False`) |
729
780
 
730
- **File naming convention:** `NNN_description.sql` where `NNN` is a zero-padded numeric version and `description` uses underscores. Files not matching this pattern raise `MigrationFileError`.
781
+ **File naming convention:** `NNN_description.sql` where `NNN` is a numeric version and `description` uses underscores. Migrations are ordered numerically (`9_a.sql` before `10_b.sql`). Files not matching this pattern, or two files with the same number, raise `MigrationFileError`.
731
782
 
732
- **Tracking:** MigrationManager creates a `schema_migrations` table to track applied versions.
783
+ **Multiple statements:** a file may contain several statements separated by `;`. Semicolons inside string literals, quoted identifiers, comments and PostgreSQL dollar-quoted bodies do not split. For procedural code with internal semicolons (MySQL/Oracle `BEGIN ... END`), add a `-- rowquery:no-split` line to run the file as a single statement.
733
784
 
734
- **Note:** The current implementation uses SQLite-specific SQL syntax for the tracking table. Multi-backend migration support may require adaptation.
785
+ **Tracking:** MigrationManager creates a portable `schema_migrations` table (SQLite, PostgreSQL, MySQL, Oracle) to track applied versions. Each migration runs in one transaction with its tracking row; MySQL and Oracle commit implicitly after DDL, so a failed migration may leave earlier statements applied there.
735
786
 
736
787
  ## Repository Pattern
737
788
 
@@ -821,7 +872,9 @@ RowQueryError
821
872
  │ └── DuplicateQueryError
822
873
  ├── ExecutionError
823
874
  │ ├── MultipleRowsError
824
- │ ├── ParameterBindingError
875
+ │ ├── QueryExecutionError
876
+ │ │ ├── ParameterBindingError
877
+ │ │ └── IntegrityError
825
878
  │ └── SQLSanitizationError
826
879
  ├── MappingError
827
880
  │ ├── ColumnMismatchError
@@ -835,6 +888,7 @@ RowQueryError
835
888
  └── AdapterError
836
889
  ├── ConnectionError
837
890
  └── PoolError
891
+ └── PoolTimeoutError
838
892
  ```
839
893
 
840
894
  **Detailed reference:**
@@ -847,7 +901,9 @@ RowQueryError
847
901
  | `DuplicateQueryError` | `RegistryError` | Two SQL files produce the same registry key | `query_name: str` |
848
902
  | `ExecutionError` | `RowQueryError` | Base for query execution errors | — |
849
903
  | `MultipleRowsError` | `ExecutionError` | `fetch_one()` returns more than one row | `query_name: str`, `row_count: int` |
850
- | `ParameterBindingError` | `ExecutionError` | Parameter binding fails during query execution | `query_name: str` |
904
+ | `QueryExecutionError` | `ExecutionError` | The database rejects a query (syntax error, missing table, ...). Driver exception in `__cause__`. | `query_name: str`, `detail: str` |
905
+ | `ParameterBindingError` | `QueryExecutionError` | Missing, extra or mistyped parameters | `query_name: str`, `detail: str` |
906
+ | `IntegrityError` | `QueryExecutionError` | Constraint violation (unique, foreign key, not null, check) | `query_name: str`, `detail: str` |
851
907
  | `SQLSanitizationError` | `ExecutionError` | SQLSanitizer rejects inline SQL | — |
852
908
  | `MappingError` | `RowQueryError` | Base for result mapping errors | — |
853
909
  | `ColumnMismatchError` | `MappingError` | Row columns don't match target class fields | `target_class: str`, `missing_fields: list[str]` |
@@ -860,7 +916,8 @@ RowQueryError
860
916
  | `MigrationExecutionError` | `MigrationError` | Migration SQL execution fails | `version: str` |
861
917
  | `AdapterError` | `RowQueryError` | Base for adapter/connection errors | — |
862
918
  | `ConnectionError` | `AdapterError` | Database connection cannot be established | — |
863
- | `PoolError` | `AdapterError` | Connection pool error (exhausted, timeout) | — |
919
+ | `PoolError` | `AdapterError` | Connection pool error (e.g. pool closed) | — |
920
+ | `PoolTimeoutError` | `PoolError` | No connection became free within `pool_timeout` | `pool_size: int`, `timeout: float` |
864
921
 
865
922
  ## Terminology Glossary
866
923
 
@@ -880,4 +937,4 @@ RowQueryError
880
937
  | **TransactionManager** | A context manager for executing multiple SQL statements atomically. Auto-commits on success, auto-rolls-back on exception. `AsyncTransactionManager` is its async counterpart. |
881
938
  | **MigrationManager** | Discovers, tracks, and applies SQL migration files in version order using a `schema_migrations` tracking table. |
882
939
  | **Repository** | A DDD-style base class for organizing database access around domain entities. Wraps an Engine and optional mapper. `AsyncRepository` is its async counterpart. |
883
- | **:name parameter** | The parameter syntax used in SQL queries (e.g., `:id`, `:name`). RowQuery automatically converts this to the format required by each backend. |
940
+ | **:name parameter** | The named parameter syntax used in SQL queries (e.g., `:id`, `:name`). RowQuery automatically converts this to the format required by each backend. Positional `?` placeholders are converted the same way. |
@@ -13,7 +13,7 @@
13
13
  - [Transactions](llms-full.txt#transactions): Sync and async transaction management
14
14
  - [Migrations](llms-full.txt#migrations): Schema migration with MigrationManager
15
15
  - [Repository Pattern](llms-full.txt#repository-pattern): DDD-style repository base classes
16
- - [Exception Reference](llms-full.txt#exception-reference): Complete exception hierarchy (18 classes)
16
+ - [Exception Reference](llms-full.txt#exception-reference): Complete exception hierarchy
17
17
  - [Terminology Glossary](llms-full.txt#terminology-glossary): Canonical terms used in RowQuery
18
18
 
19
19
  ## Optional
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rowquery"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  description = "A SQL-first Python library for querying and mapping data across multiple database backends."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -27,11 +27,11 @@ dependencies = [
27
27
  ]
28
28
 
29
29
  [project.urls]
30
- Homepage = "https://github.com/maksim-shevtsov/RowQuery"
31
- Documentation = "https://github.com/maksim-shevtsov/RowQuery#readme"
32
- Repository = "https://github.com/maksim-shevtsov/RowQuery.git"
33
- Issues = "https://github.com/maksim-shevtsov/RowQuery/issues"
34
- Changelog = "https://github.com/maksim-shevtsov/RowQuery/blob/main/CHANGELOG.md"
30
+ Homepage = "https://github.com/MaksimShevtsov/RowQuery"
31
+ Documentation = "https://github.com/MaksimShevtsov/RowQuery#readme"
32
+ Repository = "https://github.com/MaksimShevtsov/RowQuery.git"
33
+ Issues = "https://github.com/MaksimShevtsov/RowQuery/issues"
34
+ Changelog = "https://github.com/MaksimShevtsov/RowQuery/blob/main/CHANGELOG.md"
35
35
 
36
36
  [project.optional-dependencies]
37
37
  postgres = ["psycopg[binary]>=3.1.0"]