sqlalchemy-firebird-async 0.2.1__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.
Files changed (34) hide show
  1. sqlalchemy_firebird_async-0.2.2/.claude/settings.local.json +9 -0
  2. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/.gitignore +1 -0
  3. sqlalchemy_firebird_async-0.2.2/CONTRIBUTING.md +58 -0
  4. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/PKG-INFO +10 -5
  5. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/README.md +6 -3
  6. sqlalchemy_firebird_async-0.2.2/justfile +11 -0
  7. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/pyproject.toml +4 -2
  8. sqlalchemy_firebird_async-0.2.2/pytest.ini +5 -0
  9. sqlalchemy_firebird_async-0.2.2/repro_cast.py +25 -0
  10. sqlalchemy_firebird_async-0.2.2/src/sqlalchemy_firebird_async/compiler.py +166 -0
  11. sqlalchemy_firebird_async-0.2.2/src/sqlalchemy_firebird_async/fdb.py +253 -0
  12. sqlalchemy_firebird_async-0.2.2/src/sqlalchemy_firebird_async/firebird_driver.py +374 -0
  13. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/src/sqlalchemy_firebird_async/firebirdsql.py +7 -2
  14. sqlalchemy_firebird_async-0.2.2/src/sqlalchemy_firebird_async/types.py +96 -0
  15. sqlalchemy_firebird_async-0.2.2/test_output.txt +2483 -0
  16. sqlalchemy_firebird_async-0.2.2/test_output_after_fix.txt +2390 -0
  17. sqlalchemy_firebird_async-0.2.2/test_output_final.txt +2181 -0
  18. sqlalchemy_firebird_async-0.2.2/test_output_full.txt +1158 -0
  19. sqlalchemy_firebird_async-0.2.2/tests/__init__.py +1 -0
  20. sqlalchemy_firebird_async-0.2.2/tests/conftest.py +211 -0
  21. sqlalchemy_firebird_async-0.2.2/tests/requirements.py +126 -0
  22. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/tests/test_basic.py +11 -11
  23. sqlalchemy_firebird_async-0.2.2/tests/test_compliance.py +21 -0
  24. sqlalchemy_firebird_async-0.2.2/tests/test_enum.py +48 -0
  25. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/tests/test_load.py +17 -17
  26. sqlalchemy_firebird_async-0.2.2/tests/test_terminate.py +60 -0
  27. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/tests/test_types.py +6 -6
  28. sqlalchemy_firebird_async-0.2.1/src/sqlalchemy_firebird_async/compiler.py +0 -12
  29. sqlalchemy_firebird_async-0.2.1/src/sqlalchemy_firebird_async/fdb.py +0 -160
  30. sqlalchemy_firebird_async-0.2.1/src/sqlalchemy_firebird_async/firebird_driver.py +0 -138
  31. sqlalchemy_firebird_async-0.2.1/tests/conftest.py +0 -69
  32. sqlalchemy_firebird_async-0.2.1/tests/test_terminate.py +0 -59
  33. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/LICENSE +0 -0
  34. {sqlalchemy_firebird_async-0.2.1 → sqlalchemy_firebird_async-0.2.2}/src/sqlalchemy_firebird_async/__init__.py +0 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(TEST_DIALECT=firebird_async uv run pytest:*)",
5
+ "Bash(TEST_DIALECT=firebird_sync uv run pytest:*)",
6
+ "Bash(grep:*)"
7
+ ]
8
+ }
9
+ }
@@ -148,3 +148,4 @@ cython_debug/
148
148
  # PyCharm
149
149
  .idea/
150
150
  uv.lock
151
+ setup.cfg
@@ -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.1
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 :: 4 - Beta
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
- ![Python Version](https://img.shields.io/pypi/pyversions/sqlalchemy-firebird-async)
36
- ![License](https://img.shields.io/pypi/l/sqlalchemy-firebird-async)
37
- ![Status](https://img.shields.io/pypi/status/sqlalchemy-firebird-async)
37
+ [![Python Version](https://img.shields.io/pypi/pyversions/sqlalchemy-firebird-async?style=for-the-badge)](https://pypi.org/project/sqlalchemy-firebird-async/)
38
+ [![License](https://img.shields.io/pypi/l/sqlalchemy-firebird-async?style=for-the-badge)](https://pypi.org/project/sqlalchemy-firebird-async/)
39
+ [![Status](https://img.shields.io/pypi/status/sqlalchemy-firebird-async?style=for-the-badge)](https://pypi.org/project/sqlalchemy-firebird-async/)
40
+ [![PyPI](https://img.shields.io/pypi/v/sqlalchemy-firebird-async?style=for-the-badge)](https://pypi.org/project/sqlalchemy-firebird-async/)
41
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/sqlalchemy-firebird-async?style=for-the-badge)](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
- ![Python Version](https://img.shields.io/pypi/pyversions/sqlalchemy-firebird-async)
4
- ![License](https://img.shields.io/pypi/l/sqlalchemy-firebird-async)
5
- ![Status](https://img.shields.io/pypi/status/sqlalchemy-firebird-async)
3
+ [![Python Version](https://img.shields.io/pypi/pyversions/sqlalchemy-firebird-async?style=for-the-badge)](https://pypi.org/project/sqlalchemy-firebird-async/)
4
+ [![License](https://img.shields.io/pypi/l/sqlalchemy-firebird-async?style=for-the-badge)](https://pypi.org/project/sqlalchemy-firebird-async/)
5
+ [![Status](https://img.shields.io/pypi/status/sqlalchemy-firebird-async?style=for-the-badge)](https://pypi.org/project/sqlalchemy-firebird-async/)
6
+ [![PyPI](https://img.shields.io/pypi/v/sqlalchemy-firebird-async?style=for-the-badge)](https://pypi.org/project/sqlalchemy-firebird-async/)
7
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/sqlalchemy-firebird-async?style=for-the-badge)](https://pypistats.org/packages/sqlalchemy-firebird-async)
8
+
6
9
 
7
10
  **Asynchronous Firebird dialect for SQLAlchemy.**
8
11
 
@@ -0,0 +1,11 @@
1
+
2
+
3
+ test:
4
+ uv run pytest
5
+
6
+ build:
7
+ uv build
8
+
9
+ publish:
10
+ export UV_PUBLISH_TOKEN=$(grep -A 5 "\[pypi\]" ~/.pypirc | grep "password" | cut -d = -f 2 | tr -d ' ')
11
+ uv publish
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlalchemy-firebird-async"
7
- version = "0.2.1"
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 :: 4 - Beta",
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,5 @@
1
+ [pytest]
2
+ addopts = --tb=native -v -r s
3
+ testpaths = tests
4
+ # Plugin for testing dialects
5
+ # Usually this is 'sqlalchemy.testing.plugin.pytestplugin'
@@ -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()