sqlalchemy-firebird-async 0.2.0__tar.gz → 0.2.2__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.
- sqlalchemy_firebird_async-0.2.2/.claude/settings.local.json +9 -0
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/.gitignore +1 -0
- sqlalchemy_firebird_async-0.2.2/CONTRIBUTING.md +58 -0
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/PKG-INFO +10 -5
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/README.md +6 -3
- sqlalchemy_firebird_async-0.2.2/justfile +11 -0
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/pyproject.toml +4 -2
- sqlalchemy_firebird_async-0.2.2/pytest.ini +5 -0
- sqlalchemy_firebird_async-0.2.2/repro_cast.py +25 -0
- sqlalchemy_firebird_async-0.2.2/src/sqlalchemy_firebird_async/compiler.py +166 -0
- sqlalchemy_firebird_async-0.2.2/src/sqlalchemy_firebird_async/fdb.py +253 -0
- sqlalchemy_firebird_async-0.2.2/src/sqlalchemy_firebird_async/firebird_driver.py +374 -0
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/src/sqlalchemy_firebird_async/firebirdsql.py +7 -2
- sqlalchemy_firebird_async-0.2.2/src/sqlalchemy_firebird_async/types.py +96 -0
- sqlalchemy_firebird_async-0.2.2/test_output.txt +2483 -0
- sqlalchemy_firebird_async-0.2.2/test_output_after_fix.txt +2390 -0
- sqlalchemy_firebird_async-0.2.2/test_output_final.txt +2181 -0
- sqlalchemy_firebird_async-0.2.2/test_output_full.txt +1158 -0
- sqlalchemy_firebird_async-0.2.2/tests/__init__.py +1 -0
- sqlalchemy_firebird_async-0.2.2/tests/conftest.py +211 -0
- sqlalchemy_firebird_async-0.2.2/tests/requirements.py +126 -0
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/tests/test_basic.py +11 -11
- sqlalchemy_firebird_async-0.2.2/tests/test_compliance.py +21 -0
- sqlalchemy_firebird_async-0.2.2/tests/test_enum.py +48 -0
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/tests/test_load.py +17 -17
- sqlalchemy_firebird_async-0.2.2/tests/test_terminate.py +60 -0
- sqlalchemy_firebird_async-0.2.2/tests/test_types.py +141 -0
- sqlalchemy_firebird_async-0.2.0/src/sqlalchemy_firebird_async/compiler.py +0 -12
- sqlalchemy_firebird_async-0.2.0/src/sqlalchemy_firebird_async/fdb.py +0 -157
- sqlalchemy_firebird_async-0.2.0/src/sqlalchemy_firebird_async/firebird_driver.py +0 -135
- sqlalchemy_firebird_async-0.2.0/tests/conftest.py +0 -69
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/LICENSE +0 -0
- {sqlalchemy_firebird_async-0.2.0 → sqlalchemy_firebird_async-0.2.2}/src/sqlalchemy_firebird_async/__init__.py +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Contributing to sqlalchemy-firebird-async
|
|
2
|
+
|
|
3
|
+
First off, thanks for taking the time to contribute!
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
1. **Clone the repository:**
|
|
8
|
+
```bash
|
|
9
|
+
git clone https://github.com/attid/sqlalchemy-firebird-async.git
|
|
10
|
+
cd sqlalchemy-firebird-async
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. **Install dependencies (using `uv` or `pip`):**
|
|
14
|
+
```bash
|
|
15
|
+
uv sync # or pip install -e .[all,test]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Running Tests
|
|
19
|
+
|
|
20
|
+
We use `pytest` and `testcontainers` (Docker) for testing. Ensure Docker is running.
|
|
21
|
+
|
|
22
|
+
### 1. Standard Tests (Recommended)
|
|
23
|
+
These tests cover the core functionality of the asynchronous drivers (Core, ORM, Types).
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Run tests for the default driver (fdb_async)
|
|
27
|
+
uv run pytest
|
|
28
|
+
|
|
29
|
+
# Run tests for the new driver (firebird_async)
|
|
30
|
+
TEST_DIALECT=firebird_async uv run pytest
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. SQLAlchemy Compliance Suite (Advanced)
|
|
34
|
+
These tests run the official SQLAlchemy test suite against our dialect. This is useful for verifying deep compatibility.
|
|
35
|
+
|
|
36
|
+
**Note:** This automatically starts a Firebird container and configures `setup.cfg`.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Run Compliance Suite using fdb_async
|
|
40
|
+
TEST_DIALECT=firebird_async uv run pytest tests/test_compliance.py -p sqlalchemy.testing.plugin.pytestplugin
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Supported Drivers
|
|
44
|
+
|
|
45
|
+
* **`fdb_async`** (Default): Uses the threaded legacy `fdb` driver. Stable but lacks some FB4 features.
|
|
46
|
+
* **`firebird_async`**: Uses the modern `firebird-driver`. Supports FB4 features (INT128, TimeZones).
|
|
47
|
+
|
|
48
|
+
## Project Structure
|
|
49
|
+
|
|
50
|
+
* `src/sqlalchemy_firebird_async/`: Source code.
|
|
51
|
+
* `fdb.py`: `fdb` based dialect.
|
|
52
|
+
* `firebird_driver.py`: `firebird-driver` based dialect.
|
|
53
|
+
* `compiler.py`: Custom type compiler patches.
|
|
54
|
+
* `tests/`: Tests.
|
|
55
|
+
* `test_basic.py`: Basic CRUD and ORM tests.
|
|
56
|
+
* `test_types.py`: Comprehensive data types tests.
|
|
57
|
+
* `test_load.py`: Concurrency load test.
|
|
58
|
+
* `test_compliance.py`: Entry point for SQLAlchemy Compliance Suite.
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-firebird-async
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Asyncio support for Firebird in SQLAlchemy
|
|
5
5
|
Project-URL: Homepage, https://github.com/attid/sqlalchemy-firebird-async
|
|
6
|
+
Project-URL: Repository, https://github.com/attid/sqlalchemy-firebird-async
|
|
7
|
+
Project-URL: Issues, https://github.com/attid/sqlalchemy-firebird-async/issues
|
|
6
8
|
Author-email: Igor Tolstov <attid0@gmail.com>
|
|
7
9
|
License: MIT
|
|
8
10
|
License-File: LICENSE
|
|
9
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
12
|
Classifier: Framework :: AsyncIO
|
|
11
13
|
Classifier: Intended Audience :: Developers
|
|
12
14
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -32,9 +34,12 @@ Description-Content-Type: text/markdown
|
|
|
32
34
|
|
|
33
35
|
# sqlalchemy-firebird-async
|
|
34
36
|
|
|
35
|
-

|
|
36
|
-

|
|
37
|
-

|
|
37
|
+
[](https://pypi.org/project/sqlalchemy-firebird-async/)
|
|
38
|
+
[](https://pypi.org/project/sqlalchemy-firebird-async/)
|
|
39
|
+
[](https://pypi.org/project/sqlalchemy-firebird-async/)
|
|
40
|
+
[](https://pypi.org/project/sqlalchemy-firebird-async/)
|
|
41
|
+
[](https://pypistats.org/packages/sqlalchemy-firebird-async)
|
|
42
|
+
|
|
38
43
|
|
|
39
44
|
**Asynchronous Firebird dialect for SQLAlchemy.**
|
|
40
45
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# sqlalchemy-firebird-async
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-

|
|
5
|
-

|
|
3
|
+
[](https://pypi.org/project/sqlalchemy-firebird-async/)
|
|
4
|
+
[](https://pypi.org/project/sqlalchemy-firebird-async/)
|
|
5
|
+
[](https://pypi.org/project/sqlalchemy-firebird-async/)
|
|
6
|
+
[](https://pypi.org/project/sqlalchemy-firebird-async/)
|
|
7
|
+
[](https://pypistats.org/packages/sqlalchemy-firebird-async)
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
**Asynchronous Firebird dialect for SQLAlchemy.**
|
|
8
11
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlalchemy-firebird-async"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Asyncio support for Firebird in SQLAlchemy"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -12,7 +12,7 @@ authors = [
|
|
|
12
12
|
]
|
|
13
13
|
license = { text = "MIT" }
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
18
|
"Framework :: AsyncIO",
|
|
@@ -42,3 +42,5 @@ test = [
|
|
|
42
42
|
|
|
43
43
|
[project.urls]
|
|
44
44
|
"Homepage" = "https://github.com/attid/sqlalchemy-firebird-async"
|
|
45
|
+
"Repository" = "https://github.com/attid/sqlalchemy-firebird-async"
|
|
46
|
+
"Issues" = "https://github.com/attid/sqlalchemy-firebird-async/issues"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import firebird.driver as driver
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
user = os.environ.get("ISC_USER", "sysdba")
|
|
5
|
+
password = os.environ.get("ISC_PASSWORD", "masterkey")
|
|
6
|
+
host = os.environ.get("ISC_HOST", "localhost")
|
|
7
|
+
database = os.environ.get("ISC_DATABASE", "/tmp/test.fdb")
|
|
8
|
+
|
|
9
|
+
dsn = f"{host}:{database}"
|
|
10
|
+
|
|
11
|
+
print(f"Connecting to {dsn} with user {user}...")
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
con = driver.connect(dsn, user=user, password=password)
|
|
15
|
+
cur = con.cursor()
|
|
16
|
+
|
|
17
|
+
# Test CAST with TIMESTAMP(6)
|
|
18
|
+
sql = "SELECT CAST('2023-01-01 10:00:00' AS TIMESTAMP(6)) FROM RDB$DATABASE"
|
|
19
|
+
print(f"Executing: {sql}")
|
|
20
|
+
cur.execute(sql)
|
|
21
|
+
print("Success!")
|
|
22
|
+
|
|
23
|
+
con.close()
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(f"Failed: {e}")
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from sqlalchemy_firebird.base import FBCompiler
|
|
2
|
+
from sqlalchemy_firebird.base import FBTypeCompiler
|
|
3
|
+
from sqlalchemy_firebird.base import FBDDLCompiler
|
|
4
|
+
from sqlalchemy.sql import elements
|
|
5
|
+
from sqlalchemy.sql import expression
|
|
6
|
+
from sqlalchemy.sql import operators
|
|
7
|
+
from sqlalchemy.sql.compiler import OPERATORS
|
|
8
|
+
|
|
9
|
+
class PatchedFBDDLCompiler(FBDDLCompiler):
|
|
10
|
+
def visit_unique_constraint(self, constraint, **kw):
|
|
11
|
+
# Firebird can be picky about quoted constraint names with spaces in some contexts
|
|
12
|
+
# but SQLAlchemy testing uses them.
|
|
13
|
+
return super().visit_unique_constraint(constraint, **kw)
|
|
14
|
+
|
|
15
|
+
class PatchedFBTypeCompiler(FBTypeCompiler):
|
|
16
|
+
def _render_string_type(self, type_, name=None, length_override=None, **kwargs):
|
|
17
|
+
if name is None and isinstance(type_, str):
|
|
18
|
+
name = type_
|
|
19
|
+
type_ = None
|
|
20
|
+
|
|
21
|
+
if length_override is None and "length" in kwargs:
|
|
22
|
+
length_override = kwargs.pop("length")
|
|
23
|
+
|
|
24
|
+
collation = kwargs.pop("collation", None)
|
|
25
|
+
|
|
26
|
+
if type_ is None:
|
|
27
|
+
text = name
|
|
28
|
+
if length_override:
|
|
29
|
+
text += f"({length_override})"
|
|
30
|
+
if collation:
|
|
31
|
+
text += f" COLLATE {collation}"
|
|
32
|
+
return text
|
|
33
|
+
|
|
34
|
+
# Fix for TypeError: unsupported operand type(s) for +: 'int' and 'str'
|
|
35
|
+
if not isinstance(name, str):
|
|
36
|
+
# Attempt to restore type name from the type object itself
|
|
37
|
+
if hasattr(type_, "__visit_name__"):
|
|
38
|
+
name = type_.__visit_name__.upper()
|
|
39
|
+
else:
|
|
40
|
+
name = "VARCHAR"
|
|
41
|
+
return super()._render_string_type(type_, name, length_override)
|
|
42
|
+
|
|
43
|
+
def visit_VARCHAR(self, type_, **kw):
|
|
44
|
+
return self._render_string_type(type_, "VARCHAR", length_override=type_.length)
|
|
45
|
+
|
|
46
|
+
def visit_CHAR(self, type_, **kw):
|
|
47
|
+
return self._render_string_type(type_, "CHAR", length_override=type_.length)
|
|
48
|
+
|
|
49
|
+
def visit_NVARCHAR(self, type_, **kw):
|
|
50
|
+
return self._render_string_type(type_, "NVARCHAR", length_override=type_.length)
|
|
51
|
+
|
|
52
|
+
def visit_NCHAR(self, type_, **kw):
|
|
53
|
+
return self._render_string_type(type_, "NCHAR", length_override=type_.length)
|
|
54
|
+
|
|
55
|
+
def visit_DOUBLE(self, type_, **kw):
|
|
56
|
+
return "DOUBLE PRECISION"
|
|
57
|
+
|
|
58
|
+
def visit_DOUBLE_PRECISION(self, type_, **kw):
|
|
59
|
+
return "DOUBLE PRECISION"
|
|
60
|
+
|
|
61
|
+
def visit_FLOAT(self, type_, **kw):
|
|
62
|
+
return "DOUBLE PRECISION"
|
|
63
|
+
|
|
64
|
+
def visit_TIMESTAMP(self, type_, **kw):
|
|
65
|
+
return "TIMESTAMP"
|
|
66
|
+
|
|
67
|
+
def visit_TIME(self, type_, **kw):
|
|
68
|
+
return "TIME"
|
|
69
|
+
|
|
70
|
+
def visit_datetime(self, type_, **kw):
|
|
71
|
+
return "TIMESTAMP"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
import re
|
|
75
|
+
|
|
76
|
+
class PatchedFBCompiler(FBCompiler):
|
|
77
|
+
def visit_bindparam(self, bindparam, within_columns_clause=False, **kwargs):
|
|
78
|
+
if within_columns_clause and bindparam.value is not None:
|
|
79
|
+
# Check for NullType FIRST to avoid CompileError
|
|
80
|
+
from sqlalchemy.types import NullType
|
|
81
|
+
if isinstance(bindparam.type, NullType):
|
|
82
|
+
return super().visit_bindparam(bindparam, within_columns_clause=within_columns_clause, **kwargs)
|
|
83
|
+
|
|
84
|
+
# Firebird needs CAST for parameters in SELECT list
|
|
85
|
+
# SELECT ? -> SELECT CAST(? AS <type>)
|
|
86
|
+
db_type = self.dialect.type_compiler.process(bindparam.type)
|
|
87
|
+
if db_type and "NULL" not in db_type.upper():
|
|
88
|
+
# Firebird CAST(.. AS TIMESTAMP(6)) is not valid, use CAST(.. AS TIMESTAMP)
|
|
89
|
+
if "TIMESTAMP" in db_type and "(6)" in db_type:
|
|
90
|
+
db_type = db_type.replace("(6)", "")
|
|
91
|
+
elif "TIME" in db_type and "(6)" in db_type:
|
|
92
|
+
db_type = db_type.replace("(6)", "")
|
|
93
|
+
|
|
94
|
+
return f"CAST({super().visit_bindparam(bindparam, within_columns_clause=within_columns_clause, **kwargs)} AS {db_type})"
|
|
95
|
+
|
|
96
|
+
ret = super().visit_bindparam(bindparam, within_columns_clause=within_columns_clause, **kwargs)
|
|
97
|
+
|
|
98
|
+
# If we are NOT in the columns clause (e.g. WHERE, INSERT values),
|
|
99
|
+
# modify the CAST(...) wrapper generated by the parent compiler.
|
|
100
|
+
# Firebird infers strict types from context (e.g. VARCHAR(2) col -> VARCHAR(2) param),
|
|
101
|
+
# which causes truncation for LIKE patterns longer than the column.
|
|
102
|
+
# We widen the CAST to VARCHAR(2000) to allow longer patterns.
|
|
103
|
+
# We avoid max length (32765) to prevent "Implementation limit exceeded" errors.
|
|
104
|
+
if not within_columns_clause and "CAST" in ret and "VARCHAR" in ret:
|
|
105
|
+
ret = re.sub(r'(VARCHAR|CHAR|NVARCHAR|NCHAR)\(\d+\)', r'\1(2000)', ret)
|
|
106
|
+
|
|
107
|
+
return ret
|
|
108
|
+
|
|
109
|
+
def order_by_clause(self, select, **kw):
|
|
110
|
+
if isinstance(select, expression.CompoundSelect):
|
|
111
|
+
return self._compound_order_by_clause(select, **kw)
|
|
112
|
+
return super().order_by_clause(select, **kw)
|
|
113
|
+
|
|
114
|
+
def _compound_order_by_clause(self, select, **kw):
|
|
115
|
+
if not select._order_by_clauses:
|
|
116
|
+
return ""
|
|
117
|
+
|
|
118
|
+
pos_by_name = {}
|
|
119
|
+
for idx, col in enumerate(select.selected_columns, 1):
|
|
120
|
+
for key in (getattr(col, "key", None), getattr(col, "name", None)):
|
|
121
|
+
if key and key not in pos_by_name:
|
|
122
|
+
pos_by_name[key] = idx
|
|
123
|
+
|
|
124
|
+
clauses = []
|
|
125
|
+
for clause in select._order_by_clauses:
|
|
126
|
+
elem = clause
|
|
127
|
+
direction = None
|
|
128
|
+
|
|
129
|
+
if isinstance(elem, elements.UnaryExpression) and elem.modifier in (
|
|
130
|
+
operators.asc_op,
|
|
131
|
+
operators.desc_op,
|
|
132
|
+
):
|
|
133
|
+
direction = elem.modifier
|
|
134
|
+
elem = elem.element
|
|
135
|
+
|
|
136
|
+
if isinstance(elem, elements._label_reference):
|
|
137
|
+
elem = elem.element
|
|
138
|
+
if isinstance(elem, elements.UnaryExpression) and elem.modifier in (
|
|
139
|
+
operators.asc_op,
|
|
140
|
+
operators.desc_op,
|
|
141
|
+
):
|
|
142
|
+
direction = elem.modifier
|
|
143
|
+
elem = elem.element
|
|
144
|
+
|
|
145
|
+
if isinstance(elem, elements._textual_label_reference):
|
|
146
|
+
key = elem.element
|
|
147
|
+
else:
|
|
148
|
+
key = getattr(elem, "key", None) or getattr(elem, "name", None)
|
|
149
|
+
|
|
150
|
+
if key in pos_by_name:
|
|
151
|
+
position = expression.literal_column(str(pos_by_name[key]))
|
|
152
|
+
if direction is operators.desc_op:
|
|
153
|
+
position = position.desc()
|
|
154
|
+
elif direction is operators.asc_op:
|
|
155
|
+
position = position.asc()
|
|
156
|
+
clauses.append(position)
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
clauses.append(clause)
|
|
160
|
+
|
|
161
|
+
order_by = self._generate_delimited_list(
|
|
162
|
+
clauses, OPERATORS[operators.comma_op], **kw
|
|
163
|
+
)
|
|
164
|
+
if order_by:
|
|
165
|
+
return " ORDER BY " + order_by
|
|
166
|
+
return ""
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from functools import partial
|
|
4
|
+
from sqlalchemy.util.concurrency import await_only
|
|
5
|
+
from greenlet import getcurrent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AsyncCursor:
|
|
9
|
+
def __init__(self, sync_cursor, loop, executor=None):
|
|
10
|
+
self._sync_cursor = sync_cursor
|
|
11
|
+
self._loop = loop
|
|
12
|
+
self._executor = executor
|
|
13
|
+
|
|
14
|
+
def _exec(self, func, *args, **kwargs):
|
|
15
|
+
# Check whether we are in a SQLAlchemy-created greenlet context.
|
|
16
|
+
in_greenlet = getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None)
|
|
17
|
+
if self._executor is not None:
|
|
18
|
+
if in_greenlet:
|
|
19
|
+
return await_only(
|
|
20
|
+
self._loop.run_in_executor(
|
|
21
|
+
self._executor, partial(func, *args, **kwargs)
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
return self._executor.submit(func, *args, **kwargs).result()
|
|
25
|
+
if in_greenlet:
|
|
26
|
+
return await_only(
|
|
27
|
+
self._loop.run_in_executor(None, partial(func, *args, **kwargs))
|
|
28
|
+
)
|
|
29
|
+
# If not, call synchronously (e.g. inside run_sync).
|
|
30
|
+
return func(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
def execute(self, operation, parameters=None):
|
|
33
|
+
if parameters is None:
|
|
34
|
+
return self._exec(self._sync_cursor.execute, operation)
|
|
35
|
+
else:
|
|
36
|
+
return self._exec(self._sync_cursor.execute, operation, parameters)
|
|
37
|
+
|
|
38
|
+
def executemany(self, operation, seq_of_parameters):
|
|
39
|
+
return self._exec(self._sync_cursor.executemany, operation, seq_of_parameters)
|
|
40
|
+
|
|
41
|
+
def fetchone(self):
|
|
42
|
+
return self._exec(self._sync_cursor.fetchone)
|
|
43
|
+
|
|
44
|
+
def fetchmany(self, size=None):
|
|
45
|
+
if size is None:
|
|
46
|
+
return self._exec(self._sync_cursor.fetchmany)
|
|
47
|
+
return self._exec(self._sync_cursor.fetchmany, size)
|
|
48
|
+
|
|
49
|
+
def fetchall(self):
|
|
50
|
+
return self._exec(self._sync_cursor.fetchall)
|
|
51
|
+
|
|
52
|
+
def close(self):
|
|
53
|
+
return self._exec(self._sync_cursor.close)
|
|
54
|
+
|
|
55
|
+
async def _async_soft_close(self):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def nextset(self):
|
|
59
|
+
return self._exec(self._sync_cursor.nextset)
|
|
60
|
+
|
|
61
|
+
def __getattr__(self, name):
|
|
62
|
+
return getattr(self._sync_cursor, name)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AsyncConnection:
|
|
66
|
+
def __init__(self, sync_connection, loop, executor=None):
|
|
67
|
+
self._sync_connection = sync_connection
|
|
68
|
+
self._loop = loop
|
|
69
|
+
self._executor = executor
|
|
70
|
+
|
|
71
|
+
def _exec(self, func, *args, **kwargs):
|
|
72
|
+
in_greenlet = getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None)
|
|
73
|
+
if self._executor is not None:
|
|
74
|
+
if in_greenlet:
|
|
75
|
+
return await_only(
|
|
76
|
+
self._loop.run_in_executor(
|
|
77
|
+
self._executor, partial(func, *args, **kwargs)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return self._executor.submit(func, *args, **kwargs).result()
|
|
81
|
+
if in_greenlet:
|
|
82
|
+
return await_only(
|
|
83
|
+
self._loop.run_in_executor(None, partial(func, *args, **kwargs))
|
|
84
|
+
)
|
|
85
|
+
return func(*args, **kwargs)
|
|
86
|
+
|
|
87
|
+
def cursor(self):
|
|
88
|
+
sync_cursor = self._exec(self._sync_connection.cursor)
|
|
89
|
+
return AsyncCursor(sync_cursor, self._loop, self._executor)
|
|
90
|
+
|
|
91
|
+
def begin(self):
|
|
92
|
+
return self._exec(self._sync_connection.begin)
|
|
93
|
+
|
|
94
|
+
def commit(self):
|
|
95
|
+
return self._exec(self._sync_connection.commit)
|
|
96
|
+
|
|
97
|
+
def rollback(self):
|
|
98
|
+
return self._exec(self._sync_connection.rollback)
|
|
99
|
+
|
|
100
|
+
def close(self):
|
|
101
|
+
try:
|
|
102
|
+
return self._exec(self._sync_connection.close)
|
|
103
|
+
finally:
|
|
104
|
+
if self._executor is not None:
|
|
105
|
+
self._executor.shutdown(wait=False)
|
|
106
|
+
self._executor = None
|
|
107
|
+
|
|
108
|
+
def terminate(self):
|
|
109
|
+
return self.close()
|
|
110
|
+
|
|
111
|
+
def __getattr__(self, name):
|
|
112
|
+
return getattr(self._sync_connection, name)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AsyncDBAPI:
|
|
116
|
+
def __init__(self, sync_dbapi):
|
|
117
|
+
self._sync_dbapi = sync_dbapi
|
|
118
|
+
self.paramstyle = getattr(sync_dbapi, "paramstyle", "qmark")
|
|
119
|
+
self.apilevel = getattr(sync_dbapi, "apilevel", "2.0")
|
|
120
|
+
self.threadsafety = getattr(sync_dbapi, "threadsafety", 0)
|
|
121
|
+
for attr in (
|
|
122
|
+
"Warning",
|
|
123
|
+
"Error",
|
|
124
|
+
"InterfaceError",
|
|
125
|
+
"DatabaseError",
|
|
126
|
+
"DataError",
|
|
127
|
+
"OperationalError",
|
|
128
|
+
"IntegrityError",
|
|
129
|
+
"InternalError",
|
|
130
|
+
"ProgrammingError",
|
|
131
|
+
"NotSupportedError",
|
|
132
|
+
):
|
|
133
|
+
if hasattr(sync_dbapi, attr):
|
|
134
|
+
setattr(self, attr, getattr(sync_dbapi, attr))
|
|
135
|
+
|
|
136
|
+
def connect(self, *args, **kwargs):
|
|
137
|
+
async_creator_fn = kwargs.pop("async_creator_fn", None)
|
|
138
|
+
loop = asyncio.get_running_loop()
|
|
139
|
+
executor = None
|
|
140
|
+
|
|
141
|
+
def _connect():
|
|
142
|
+
if async_creator_fn is not None:
|
|
143
|
+
# We cannot call await_only directly if the creator is async.
|
|
144
|
+
# But fdb is synchronous, so it is fine.
|
|
145
|
+
# If async_creator is provided, it is for firebirdsql, but we are in fdb.py.
|
|
146
|
+
return async_creator_fn(*args, **kwargs) # Returns a coroutine? No, this is a callback.
|
|
147
|
+
else:
|
|
148
|
+
return self._sync_dbapi.connect(*args, **kwargs)
|
|
149
|
+
|
|
150
|
+
if getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None):
|
|
151
|
+
# If async_creator_fn returns a coroutine, await_only will wait for it.
|
|
152
|
+
# But for fdb this is sync, so use run_in_executor.
|
|
153
|
+
executor = ThreadPoolExecutor(max_workers=1)
|
|
154
|
+
try:
|
|
155
|
+
sync_conn = await_only(loop.run_in_executor(executor, _connect))
|
|
156
|
+
except Exception:
|
|
157
|
+
executor.shutdown(wait=False)
|
|
158
|
+
raise
|
|
159
|
+
else:
|
|
160
|
+
sync_conn = _connect()
|
|
161
|
+
|
|
162
|
+
return AsyncConnection(sync_conn, loop, executor)
|
|
163
|
+
|
|
164
|
+
from sqlalchemy.pool import AsyncAdaptedQueuePool
|
|
165
|
+
from sqlalchemy_firebird.base import FBExecutionContext
|
|
166
|
+
import sqlalchemy_firebird.fdb as fdb
|
|
167
|
+
from .compiler import PatchedFBCompiler, PatchedFBDDLCompiler, PatchedFBTypeCompiler
|
|
168
|
+
from .types import FBCHARCompat, FBVARCHARCompat
|
|
169
|
+
from sqlalchemy import String, DateTime, Time, TIMESTAMP, VARCHAR, CHAR
|
|
170
|
+
from .types import _FBSafeString, FBDateTime, FBTime, FBTimestamp
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class AsyncFDBExecutionContext(FBExecutionContext):
|
|
174
|
+
def post_exec(self):
|
|
175
|
+
super().post_exec()
|
|
176
|
+
if self.isddl:
|
|
177
|
+
# Firebird with fdb requires a new transaction to see DDL changes.
|
|
178
|
+
dbapi_conn = self._dbapi_connection
|
|
179
|
+
driver_conn = getattr(dbapi_conn, "driver_connection", None)
|
|
180
|
+
if driver_conn is None:
|
|
181
|
+
driver_conn = getattr(dbapi_conn, "dbapi_connection", dbapi_conn)
|
|
182
|
+
try:
|
|
183
|
+
self.cursor.close()
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
driver_conn.commit()
|
|
187
|
+
driver_conn.begin()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class AsyncFDBDialect(fdb.FBDialect_fdb):
|
|
191
|
+
name = "firebird.fdb_async"
|
|
192
|
+
driver = "fdb_async"
|
|
193
|
+
is_async = True
|
|
194
|
+
supports_statement_cache = False
|
|
195
|
+
poolclass = AsyncAdaptedQueuePool
|
|
196
|
+
statement_compiler = PatchedFBCompiler
|
|
197
|
+
ddl_compiler = PatchedFBDDLCompiler
|
|
198
|
+
execution_ctx_cls = AsyncFDBExecutionContext
|
|
199
|
+
ischema_names = fdb.FBDialect_fdb.ischema_names.copy()
|
|
200
|
+
ischema_names["TEXT"] = FBCHARCompat
|
|
201
|
+
ischema_names["VARYING"] = FBVARCHARCompat
|
|
202
|
+
ischema_names["CSTRING"] = FBVARCHARCompat
|
|
203
|
+
|
|
204
|
+
colspecs = fdb.FBDialect_fdb.colspecs.copy()
|
|
205
|
+
colspecs[String] = _FBSafeString
|
|
206
|
+
colspecs[VARCHAR] = _FBSafeString
|
|
207
|
+
colspecs[CHAR] = _FBSafeString
|
|
208
|
+
colspecs[DateTime] = FBDateTime
|
|
209
|
+
colspecs[Time] = FBTime
|
|
210
|
+
colspecs[TIMESTAMP] = FBTimestamp
|
|
211
|
+
|
|
212
|
+
# Explicitly set type compiler to ensure our patch is used
|
|
213
|
+
def __init__(self, *args, **kwargs):
|
|
214
|
+
super().__init__(*args, **kwargs)
|
|
215
|
+
self.type_compiler_instance = PatchedFBTypeCompiler(self)
|
|
216
|
+
self.type_compiler = self.type_compiler_instance
|
|
217
|
+
|
|
218
|
+
def dbapi_exception_translation(self, exception, statement, parameters, context):
|
|
219
|
+
from sqlalchemy import exc
|
|
220
|
+
|
|
221
|
+
msg = str(exception).lower()
|
|
222
|
+
if "violation" in msg and ("primary" in msg or "unique" in msg or "foreign" in msg or "constraint" in msg):
|
|
223
|
+
return exc.IntegrityError(statement, parameters, exception)
|
|
224
|
+
|
|
225
|
+
return super().dbapi_exception_translation(exception, statement, parameters, context)
|
|
226
|
+
|
|
227
|
+
def wrap_dbapi_exception(self, e, statement, parameters, cursor, context):
|
|
228
|
+
from sqlalchemy import exc
|
|
229
|
+
|
|
230
|
+
msg = str(e).lower()
|
|
231
|
+
if "violation" in msg and ("primary" in msg or "unique" in msg or "foreign" in msg or "constraint" in msg):
|
|
232
|
+
return exc.IntegrityError(statement, parameters, e)
|
|
233
|
+
|
|
234
|
+
return super().wrap_dbapi_exception(e, statement, parameters, cursor, context)
|
|
235
|
+
|
|
236
|
+
def is_disconnect(self, e, connection, cursor):
|
|
237
|
+
# Handle fdb disconnect errors which store error code in args[1]
|
|
238
|
+
# Base implementation checks for self.driver == "fdb"
|
|
239
|
+
if isinstance(e, self.dbapi.DatabaseError):
|
|
240
|
+
# We are essentially fdb
|
|
241
|
+
return (e.args[1] in (335546001, 335546003, 335546005)) or \
|
|
242
|
+
("Error writing data to the connection" in str(e))
|
|
243
|
+
return super().is_disconnect(e, connection, cursor)
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def import_dbapi(cls):
|
|
247
|
+
import fdb as sync_fdb
|
|
248
|
+
|
|
249
|
+
return AsyncDBAPI(sync_fdb)
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def dbapi(cls):
|
|
253
|
+
return cls.import_dbapi()
|