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.
- {rowquery-0.1.2 → rowquery-0.2.0}/.github/ISSUE_TEMPLATE/config.yml +1 -1
- rowquery-0.2.0/.github/workflows/test.yml +115 -0
- rowquery-0.2.0/CHANGELOG.md +85 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/CLAUDE.md +9 -3
- {rowquery-0.1.2 → rowquery-0.2.0}/CONTRIBUTING.md +1 -1
- {rowquery-0.1.2 → rowquery-0.2.0}/PKG-INFO +13 -9
- {rowquery-0.1.2 → rowquery-0.2.0}/README.md +6 -2
- {rowquery-0.1.2 → rowquery-0.2.0}/llms-full.txt +88 -31
- {rowquery-0.1.2 → rowquery-0.2.0}/llms.txt +1 -1
- {rowquery-0.1.2 → rowquery-0.2.0}/pyproject.toml +6 -6
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/__init__.py +6 -0
- rowquery-0.2.0/row_query/adapters/mysql.py +109 -0
- rowquery-0.2.0/row_query/adapters/oracle.py +123 -0
- rowquery-0.2.0/row_query/adapters/postgresql.py +91 -0
- rowquery-0.2.0/row_query/adapters/protocol.py +106 -0
- rowquery-0.2.0/row_query/adapters/sqlite.py +85 -0
- rowquery-0.2.0/row_query/core/connection.py +220 -0
- rowquery-0.2.0/row_query/core/engine.py +128 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/core/exceptions.py +33 -3
- rowquery-0.2.0/row_query/core/execution.py +340 -0
- rowquery-0.2.0/row_query/core/lexer.py +183 -0
- rowquery-0.2.0/row_query/core/migration.py +198 -0
- rowquery-0.2.0/row_query/core/params.py +234 -0
- rowquery-0.2.0/row_query/core/pool.py +271 -0
- rowquery-0.2.0/row_query/core/sanitizer.py +206 -0
- rowquery-0.2.0/row_query/core/transaction.py +192 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/aggregate.py +90 -28
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/builder.py +65 -25
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/plan.py +5 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/contract/test_adapter_protocol.py +6 -20
- rowquery-0.2.0/tests/integration/test_server_backends.py +236 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_builder.py +0 -15
- rowquery-0.2.0/tests/unit/test_engine_regressions.py +164 -0
- rowquery-0.2.0/tests/unit/test_nested_collections.py +314 -0
- rowquery-0.2.0/tests/unit/test_pool.py +184 -0
- rowquery-0.2.0/tests/unit/test_review_regressions.py +390 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_sanitizer.py +3 -5
- rowquery-0.1.2/.github/workflows/test.yml +0 -48
- rowquery-0.1.2/CHANGELOG.md +0 -48
- rowquery-0.1.2/row_query/adapters/mysql.py +0 -111
- rowquery-0.1.2/row_query/adapters/oracle.py +0 -120
- rowquery-0.1.2/row_query/adapters/postgresql.py +0 -104
- rowquery-0.1.2/row_query/adapters/protocol.py +0 -81
- rowquery-0.1.2/row_query/adapters/sqlite.py +0 -98
- rowquery-0.1.2/row_query/core/connection.py +0 -132
- rowquery-0.1.2/row_query/core/engine.py +0 -419
- rowquery-0.1.2/row_query/core/migration.py +0 -148
- rowquery-0.1.2/row_query/core/params.py +0 -122
- rowquery-0.1.2/row_query/core/sanitizer.py +0 -233
- rowquery-0.1.2/row_query/core/transaction.py +0 -334
- {rowquery-0.1.2 → rowquery-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/.github/dependabot.yml +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/.github/workflows/quality.yml +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/.github/workflows/release.yml +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/.gitignore +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/.pre-commit-config.yaml +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/CODE_OF_CONDUCT.md +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/LICENSE +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/examples/01_basic_query.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/examples/02_model_mapping.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/examples/03_aggregate.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/examples/04_transactions.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/examples/05_async.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/examples/06_migrations.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/examples/07_repository.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/examples/README.md +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/adapters/__init__.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/core/__init__.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/core/enums.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/core/registry.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/__init__.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/model.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/mapping/protocol.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/repository/__init__.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/row_query/repository/base.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/__init__.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/conftest.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/contract/__init__.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/billing/invoice/list.sql +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/user/count.sql +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/user/get_by_id.sql +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/user/insert.sql +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/fixtures/sql/user/list.sql +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/integration/__init__.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/integration/test_sqlite.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/__init__.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_aggregate_mapper.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_engine.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_migration.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_model_mapper.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_params.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_registry.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_repository.py +0 -0
- {rowquery-0.1.2 → rowquery-0.2.0}/tests/unit/test_transaction.py +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
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/
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: rowquery
|
|
3
|
-
Version: 0.
|
|
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/
|
|
6
|
-
Project-URL: Documentation, https://github.com/
|
|
7
|
-
Project-URL: Repository, https://github.com/
|
|
8
|
-
Project-URL: Issues, https://github.com/
|
|
9
|
-
Project-URL: Changelog, https://github.com/
|
|
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
|
[](https://pypi.org/project/rowquery/)
|
|
53
53
|
[](https://pypi.org/project/rowquery/)
|
|
54
54
|
[](https://opensource.org/licenses/MIT)
|
|
55
|
-
[](https://github.com/MaksimShevtsov/RowQuery/actions/workflows/test.yml)
|
|
56
|
+
[](https://codecov.io/gh/MaksimShevtsov/RowQuery)
|
|
57
57
|
[](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
|
[](https://pypi.org/project/rowquery/)
|
|
4
4
|
[](https://pypi.org/project/rowquery/)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://github.com/MaksimShevtsov/RowQuery/actions/workflows/test.yml)
|
|
7
|
+
[](https://codecov.io/gh/MaksimShevtsov/RowQuery)
|
|
8
8
|
[](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.
|
|
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
|
|
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
|
-
```
|
|
35
|
+
```bash
|
|
36
36
|
# Using uv (recommended)
|
|
37
|
-
# uv add
|
|
37
|
+
# uv add rowquery
|
|
38
38
|
|
|
39
39
|
# Using pip
|
|
40
|
-
# pip install
|
|
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
|
|
199
|
-
- **Inline SQL
|
|
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
|
|
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
|
|
249
|
-
| `pool_timeout` | `
|
|
250
|
-
| `pool_recycle` | `
|
|
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.
|
|
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)
|
|
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
|
|
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
|
|
633
|
-
| `fetch_all` | `(query, params=None)` | `list[dict[
|
|
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
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
│ ├──
|
|
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
|
-
| `
|
|
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 (
|
|
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
|
|
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.
|
|
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/
|
|
31
|
-
Documentation = "https://github.com/
|
|
32
|
-
Repository = "https://github.com/
|
|
33
|
-
Issues = "https://github.com/
|
|
34
|
-
Changelog = "https://github.com/
|
|
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"]
|