sqlalchemy-firebird-async 0.2.1__py2.py3-none-any.whl

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.
File without changes
@@ -0,0 +1,12 @@
1
+ from sqlalchemy_firebird.base import FBTypeCompiler
2
+
3
+ class PatchedFBTypeCompiler(FBTypeCompiler):
4
+ def _render_string_type(self, type_, name, length_override=None):
5
+ # Fix for TypeError: unsupported operand type(s) for +: 'int' and 'str'
6
+ if not isinstance(name, str):
7
+ # Attempt to restore type name from the type object itself
8
+ if hasattr(type_, "__visit_name__"):
9
+ name = type_.__visit_name__.upper()
10
+ else:
11
+ name = "VARCHAR"
12
+ return super()._render_string_type(type_, name, length_override)
@@ -0,0 +1,160 @@
1
+ import asyncio
2
+ from functools import partial
3
+ from sqlalchemy.util.concurrency import await_only
4
+ from greenlet import getcurrent
5
+
6
+
7
+ class AsyncCursor:
8
+ def __init__(self, sync_cursor, loop):
9
+ self._sync_cursor = sync_cursor
10
+ self._loop = loop
11
+
12
+ def _exec(self, func, *args, **kwargs):
13
+ # Проверяем, находимся ли мы в контексте greenlet, созданном SQLAlchemy
14
+ if getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None):
15
+ return await_only(self._loop.run_in_executor(None, partial(func, *args, **kwargs)))
16
+ else:
17
+ # Если нет, вызываем синхронно (например, внутри run_sync)
18
+ return func(*args, **kwargs)
19
+
20
+ def execute(self, operation, parameters=None):
21
+ if parameters is None:
22
+ return self._exec(self._sync_cursor.execute, operation)
23
+ else:
24
+ return self._exec(self._sync_cursor.execute, operation, parameters)
25
+
26
+ def executemany(self, operation, seq_of_parameters):
27
+ return self._exec(self._sync_cursor.executemany, operation, seq_of_parameters)
28
+
29
+ def fetchone(self):
30
+ return self._exec(self._sync_cursor.fetchone)
31
+
32
+ def fetchmany(self, size=None):
33
+ if size is None:
34
+ return self._exec(self._sync_cursor.fetchmany)
35
+ return self._exec(self._sync_cursor.fetchmany, size)
36
+
37
+ def fetchall(self):
38
+ return self._exec(self._sync_cursor.fetchall)
39
+
40
+ def close(self):
41
+ return self._exec(self._sync_cursor.close)
42
+
43
+ async def _async_soft_close(self):
44
+ pass
45
+
46
+ def nextset(self):
47
+ return self._exec(self._sync_cursor.nextset)
48
+
49
+ def __getattr__(self, name):
50
+ return getattr(self._sync_cursor, name)
51
+
52
+
53
+ class AsyncConnection:
54
+ def __init__(self, sync_connection, loop):
55
+ self._sync_connection = sync_connection
56
+ self._loop = loop
57
+
58
+ def _exec(self, func, *args, **kwargs):
59
+ if getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None):
60
+ return await_only(self._loop.run_in_executor(None, partial(func, *args, **kwargs)))
61
+ else:
62
+ return func(*args, **kwargs)
63
+
64
+ def cursor(self):
65
+ return AsyncCursor(self._sync_connection.cursor(), self._loop)
66
+
67
+ def commit(self):
68
+ return self._exec(self._sync_connection.commit)
69
+
70
+ def rollback(self):
71
+ return self._exec(self._sync_connection.rollback)
72
+
73
+ def close(self):
74
+ return self._exec(self._sync_connection.close)
75
+
76
+ def terminate(self):
77
+ return self._exec(self._sync_connection.close)
78
+
79
+ def __getattr__(self, name):
80
+ return getattr(self._sync_connection, name)
81
+
82
+
83
+ class AsyncDBAPI:
84
+ def __init__(self, sync_dbapi):
85
+ self._sync_dbapi = sync_dbapi
86
+ self.paramstyle = getattr(sync_dbapi, "paramstyle", "qmark")
87
+ self.apilevel = getattr(sync_dbapi, "apilevel", "2.0")
88
+ self.threadsafety = getattr(sync_dbapi, "threadsafety", 0)
89
+ for attr in (
90
+ "Warning",
91
+ "Error",
92
+ "InterfaceError",
93
+ "DatabaseError",
94
+ "DataError",
95
+ "OperationalError",
96
+ "IntegrityError",
97
+ "InternalError",
98
+ "ProgrammingError",
99
+ "NotSupportedError",
100
+ ):
101
+ if hasattr(sync_dbapi, attr):
102
+ setattr(self, attr, getattr(sync_dbapi, attr))
103
+
104
+ def connect(self, *args, **kwargs):
105
+ async_creator_fn = kwargs.pop("async_creator_fn", None)
106
+ loop = asyncio.get_running_loop()
107
+
108
+ def _connect():
109
+ if async_creator_fn is not None:
110
+ # Здесь мы не можем просто так вызвать await_only, если creator асинхронный
111
+ # Но fdb синхронный, так что всё ок.
112
+ # Если передан async_creator, то это для firebirdsql, но мы в fdb.py
113
+ return async_creator_fn(*args, **kwargs) # вернет корутину? нет, это коллбек
114
+ else:
115
+ return self._sync_dbapi.connect(*args, **kwargs)
116
+
117
+ if getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None):
118
+ # Если async_creator_fn возвращает корутину, то await_only ее дождется
119
+ # Но для fdb это синхронный вызов, поэтому run_in_executor
120
+ sync_conn = await_only(loop.run_in_executor(None, partial(self._sync_dbapi.connect, *args, **kwargs)))
121
+ else:
122
+ sync_conn = self._sync_dbapi.connect(*args, **kwargs)
123
+
124
+ return AsyncConnection(sync_conn, loop)
125
+
126
+ from sqlalchemy.pool import AsyncAdaptedQueuePool
127
+ import sqlalchemy_firebird.fdb as fdb
128
+ from .compiler import PatchedFBTypeCompiler
129
+
130
+
131
+ class AsyncFDBDialect(fdb.FBDialect_fdb):
132
+ name = "firebird.fdb_async"
133
+ driver = "fdb_async"
134
+ is_async = True
135
+ supports_statement_cache = False
136
+ poolclass = AsyncAdaptedQueuePool
137
+ # Explicitly set type compiler to ensure our patch is used
138
+ def __init__(self, *args, **kwargs):
139
+ super().__init__(*args, **kwargs)
140
+ self.type_compiler_instance = PatchedFBTypeCompiler(self)
141
+ self.type_compiler = self.type_compiler_instance
142
+
143
+ def is_disconnect(self, e, connection, cursor):
144
+ # Handle fdb disconnect errors which store error code in args[1]
145
+ # Base implementation checks for self.driver == "fdb"
146
+ if isinstance(e, self.dbapi.DatabaseError):
147
+ # We are essentially fdb
148
+ return (e.args[1] in (335546001, 335546003, 335546005)) or \
149
+ ("Error writing data to the connection" in str(e))
150
+ return super().is_disconnect(e, connection, cursor)
151
+
152
+ @classmethod
153
+ def import_dbapi(cls):
154
+ import fdb as sync_fdb
155
+
156
+ return AsyncDBAPI(sync_fdb)
157
+
158
+ @classmethod
159
+ def dbapi(cls):
160
+ return cls.import_dbapi()
@@ -0,0 +1,138 @@
1
+ import asyncio
2
+ from functools import partial
3
+ from sqlalchemy.util.concurrency import await_only
4
+ from greenlet import getcurrent
5
+ from sqlalchemy.pool import AsyncAdaptedQueuePool
6
+ import sqlalchemy_firebird.firebird as firebird_sync
7
+ import firebird.driver as sync_driver
8
+
9
+
10
+ class AsyncCursor:
11
+ def __init__(self, sync_cursor, loop):
12
+ self._sync_cursor = sync_cursor
13
+ self._loop = loop
14
+
15
+ def _exec(self, func, *args, **kwargs):
16
+ if getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None):
17
+ return await_only(self._loop.run_in_executor(None, partial(func, *args, **kwargs)))
18
+ else:
19
+ return func(*args, **kwargs)
20
+
21
+ def execute(self, operation, parameters=None):
22
+ if parameters is None:
23
+ return self._exec(self._sync_cursor.execute, operation)
24
+ else:
25
+ return self._exec(self._sync_cursor.execute, operation, parameters)
26
+
27
+ def executemany(self, operation, seq_of_parameters):
28
+ return self._exec(self._sync_cursor.executemany, operation, seq_of_parameters)
29
+
30
+ def fetchone(self):
31
+ return self._exec(self._sync_cursor.fetchone)
32
+
33
+ def fetchmany(self, size=None):
34
+ if size is None:
35
+ return self._exec(self._sync_cursor.fetchmany)
36
+ return self._exec(self._sync_cursor.fetchmany, size)
37
+
38
+ def fetchall(self):
39
+ return self._exec(self._sync_cursor.fetchall)
40
+
41
+ def close(self):
42
+ return self._exec(self._sync_cursor.close)
43
+
44
+ async def _async_soft_close(self):
45
+ pass
46
+
47
+ def nextset(self):
48
+ return self._exec(self._sync_cursor.nextset)
49
+
50
+ def __getattr__(self, name):
51
+ return getattr(self._sync_cursor, name)
52
+
53
+
54
+ class AsyncConnection:
55
+ def __init__(self, sync_connection, loop):
56
+ self._sync_connection = sync_connection
57
+ self._loop = loop
58
+
59
+ def _exec(self, func, *args, **kwargs):
60
+ if getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None):
61
+ return await_only(self._loop.run_in_executor(None, partial(func, *args, **kwargs)))
62
+ else:
63
+ return func(*args, **kwargs)
64
+
65
+ def cursor(self):
66
+ return AsyncCursor(self._sync_connection.cursor(), self._loop)
67
+
68
+ def commit(self):
69
+ return self._exec(self._sync_connection.commit)
70
+
71
+ def rollback(self):
72
+ return self._exec(self._sync_connection.rollback)
73
+
74
+ def close(self):
75
+ return self._exec(self._sync_connection.close)
76
+
77
+ def terminate(self):
78
+ return self._exec(self._sync_connection.close)
79
+
80
+ def __getattr__(self, name):
81
+ return getattr(self._sync_connection, name)
82
+
83
+
84
+ class AsyncDBAPI:
85
+ def __init__(self, sync_dbapi):
86
+ self._sync_dbapi = sync_dbapi
87
+ self.paramstyle = getattr(sync_dbapi, "paramstyle", "qmark")
88
+ self.apilevel = getattr(sync_dbapi, "apilevel", "2.0")
89
+ self.threadsafety = getattr(sync_dbapi, "threadsafety", 1)
90
+ for attr in (
91
+ "Warning",
92
+ "Error",
93
+ "InterfaceError",
94
+ "DatabaseError",
95
+ "DataError",
96
+ "OperationalError",
97
+ "IntegrityError",
98
+ "InternalError",
99
+ "ProgrammingError",
100
+ "NotSupportedError",
101
+ ):
102
+ if hasattr(sync_dbapi, attr):
103
+ setattr(self, attr, getattr(sync_dbapi, attr))
104
+
105
+ def connect(self, *args, **kwargs):
106
+ async_creator_fn = kwargs.pop("async_creator_fn", None)
107
+ loop = asyncio.get_running_loop()
108
+
109
+ if getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None):
110
+ sync_conn = await_only(loop.run_in_executor(None, partial(self._sync_dbapi.connect, *args, **kwargs)))
111
+ else:
112
+ sync_conn = self._sync_dbapi.connect(*args, **kwargs)
113
+
114
+ return AsyncConnection(sync_conn, loop)
115
+
116
+
117
+ from .compiler import PatchedFBTypeCompiler
118
+
119
+
120
+ class AsyncFirebirdDialect(firebird_sync.FBDialect_firebird):
121
+ name = "firebird.firebird_async"
122
+ driver = "firebird_async"
123
+ is_async = True
124
+ supports_statement_cache = False
125
+ poolclass = AsyncAdaptedQueuePool
126
+
127
+ def __init__(self, *args, **kwargs):
128
+ super().__init__(*args, **kwargs)
129
+ self.type_compiler_instance = PatchedFBTypeCompiler(self)
130
+ self.type_compiler = self.type_compiler_instance
131
+
132
+ @classmethod
133
+ def import_dbapi(cls):
134
+ return AsyncDBAPI(sync_driver)
135
+
136
+ @classmethod
137
+ def dbapi(cls):
138
+ return cls.import_dbapi()
@@ -0,0 +1,190 @@
1
+ import asyncio
2
+ from sqlalchemy import util
3
+ from sqlalchemy.pool import AsyncAdaptedQueuePool
4
+ from sqlalchemy.util.concurrency import await_only
5
+ from greenlet import getcurrent
6
+
7
+ import firebirdsql
8
+ import firebirdsql.aio as aio
9
+ import sqlalchemy_firebird.fdb as fdb
10
+ from .compiler import PatchedFBTypeCompiler
11
+
12
+
13
+ def _await_if_needed(value, loop):
14
+ if asyncio.iscoroutine(value):
15
+ if getattr(getcurrent(), "__sqlalchemy_greenlet_provider__", None):
16
+ return await_only(value)
17
+ else:
18
+ # We are in a sync context (e.g. run_sync), but need to await a coroutine.
19
+ # Since the loop is running in another thread, we can use run_coroutine_threadsafe.
20
+ future = asyncio.run_coroutine_threadsafe(value, loop)
21
+ return future.result()
22
+ return value
23
+
24
+
25
+ class AsyncPyfbCursor:
26
+ def __init__(self, async_cursor, loop):
27
+ self._async_cursor = async_cursor
28
+ self._loop = loop
29
+
30
+ def execute(self, operation, parameters=None):
31
+ if parameters is None:
32
+ _await_if_needed(self._async_cursor.execute(operation), self._loop)
33
+ else:
34
+ _await_if_needed(self._async_cursor.execute(operation, parameters), self._loop)
35
+ return self
36
+
37
+ def executemany(self, operation, seq_of_parameters):
38
+ _await_if_needed(self._async_cursor.executemany(operation, seq_of_parameters), self._loop)
39
+ return self
40
+
41
+ def fetchone(self):
42
+ return _await_if_needed(self._async_cursor.fetchone(), self._loop)
43
+
44
+ def fetchmany(self, size=None):
45
+ return _await_if_needed(self._async_cursor.fetchmany(size), self._loop)
46
+
47
+ def fetchall(self):
48
+ return _await_if_needed(self._async_cursor.fetchall(), self._loop)
49
+
50
+ def close(self):
51
+ return _await_if_needed(self._async_cursor.close(), self._loop)
52
+
53
+ async def _async_soft_close(self):
54
+ pass
55
+
56
+ def __getattr__(self, name):
57
+ return getattr(self._async_cursor, name)
58
+
59
+
60
+ class AsyncPyfbConnection:
61
+ def __init__(self, async_connection, loop):
62
+ self._async_connection = async_connection
63
+ self._loop = loop
64
+
65
+ def cursor(self):
66
+ return AsyncPyfbCursor(self._async_connection.cursor(), self._loop)
67
+
68
+ def commit(self):
69
+ _await_if_needed(self._async_connection.commit(), self._loop)
70
+
71
+ def rollback(self):
72
+ _await_if_needed(self._async_connection.rollback(), self._loop)
73
+
74
+ def close(self):
75
+ try:
76
+ return _await_if_needed(self._async_connection.close(), self._loop)
77
+ except BlockingIOError:
78
+ # Fallback logic from original code
79
+ sock = getattr(self._async_connection, "sock", None)
80
+ raw_sock = getattr(sock, "_sock", None)
81
+ if raw_sock is not None:
82
+ try:
83
+ raw_sock.setblocking(True)
84
+ except Exception:
85
+ pass
86
+ try:
87
+ # Synchronous close attempt if possible?
88
+ # firebirdsql.aio connection close is async.
89
+ # If we are here, something is wrong.
90
+ # Just ignore for now or try standard close if it has one?
91
+ pass
92
+ except Exception:
93
+ pass
94
+ return None
95
+
96
+ def __getattr__(self, name):
97
+ return getattr(self._async_connection, name)
98
+
99
+
100
+ class AsyncPyfbDBAPI:
101
+ def __init__(self):
102
+ self.paramstyle = getattr(firebirdsql, "paramstyle", "qmark")
103
+ self.apilevel = getattr(firebirdsql, "apilevel", "2.0")
104
+ self.threadsafety = getattr(firebirdsql, "threadsafety", 0)
105
+ for attr in (
106
+ "Warning",
107
+ "Error",
108
+ "InterfaceError",
109
+ "DatabaseError",
110
+ "DataError",
111
+ "OperationalError",
112
+ "IntegrityError",
113
+ "InternalError",
114
+ "ProgrammingError",
115
+ "NotSupportedError",
116
+ ):
117
+ if hasattr(firebirdsql, attr):
118
+ setattr(self, attr, getattr(firebirdsql, attr))
119
+
120
+ def connect(self, *args, **kwargs):
121
+ async_creator_fn = kwargs.pop("async_creator_fn", None)
122
+ loop = asyncio.get_running_loop()
123
+
124
+ # We are likely in a greenlet here (engine.connect)
125
+ if async_creator_fn is None:
126
+ async_creator_fn = aio.connect
127
+
128
+ async_connection = await_only(async_creator_fn(*args, **kwargs))
129
+ return AsyncPyfbConnection(async_connection, loop)
130
+
131
+ def Binary(self, value):
132
+ return firebirdsql.Binary(value)
133
+
134
+
135
+ class AsyncFirebirdSQLDialect(fdb.FBDialect_fdb):
136
+ name = "firebird.firebirdsql_async"
137
+ driver = "firebirdsql_async"
138
+ is_async = True
139
+ supports_statement_cache = False
140
+ poolclass = AsyncAdaptedQueuePool
141
+
142
+ def __init__(self, *args, **kwargs):
143
+ super().__init__(*args, **kwargs)
144
+ self.type_compiler_instance = PatchedFBTypeCompiler(self)
145
+ self.type_compiler = self.type_compiler_instance
146
+
147
+ @classmethod
148
+ def import_dbapi(cls):
149
+ return AsyncPyfbDBAPI()
150
+
151
+ @classmethod
152
+ def dbapi(cls):
153
+ return cls.import_dbapi()
154
+
155
+ def create_connect_args(self, url):
156
+ opts = url.translate_connect_args(username="user")
157
+ opts.update(url.query)
158
+ util.coerce_kw_type(opts, "port", int)
159
+ return ([], opts)
160
+
161
+ def _get_server_version_info(self, connection):
162
+ # Override to avoid issues with async scalar execution during dialect initialization
163
+ try:
164
+ # We can try to use the connection to execute SQL
165
+ # But in async mode, this might be tricky if not in greenlet.
166
+ # Assuming connection is AsyncPyfbConnection wrapper.
167
+
168
+ # Simple workaround: return a dummy version or suppress error
169
+ # Or try to execute:
170
+ res = connection.exec_driver_sql(
171
+ "select rdb$get_context('SYSTEM','ENGINE_VERSION') from rdb$database"
172
+ )
173
+ val = res.scalar()
174
+ # exec_driver_sql returns CursorResult. scalar() triggers fetchone().
175
+ # Our fetchone handles both sync/async contexts now.
176
+ version_str = val
177
+
178
+ except Exception:
179
+ return (0, 0)
180
+
181
+ if not version_str:
182
+ return (0, 0)
183
+
184
+ parts = str(version_str).split(".")
185
+ try:
186
+ major = int(parts[0])
187
+ minor = int(parts[1]) if len(parts) > 1 else 0
188
+ return (major, minor)
189
+ except (ValueError, IndexError):
190
+ return (0, 0)
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-firebird-async
3
+ Version: 0.2.1
4
+ Summary: Asyncio support for Firebird in SQLAlchemy
5
+ Project-URL: Homepage, https://github.com/attid/sqlalchemy-firebird-async
6
+ Author-email: Igor Tolstov <attid0@gmail.com>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Database
14
+ Requires-Dist: greenlet!=0.4.17
15
+ Requires-Dist: sqlalchemy-firebird>=2.0.0
16
+ Requires-Dist: sqlalchemy>=2.0.0
17
+ Provides-Extra: all
18
+ Requires-Dist: fdb>=2.0; extra == 'all'
19
+ Requires-Dist: firebird-driver>=1.0; extra == 'all'
20
+ Requires-Dist: firebirdsql>=1.0; extra == 'all'
21
+ Provides-Extra: fdb
22
+ Requires-Dist: fdb>=2.0; extra == 'fdb'
23
+ Provides-Extra: firebird-driver
24
+ Requires-Dist: firebird-driver>=1.0; extra == 'firebird-driver'
25
+ Provides-Extra: firebirdsql
26
+ Requires-Dist: firebirdsql>=1.0; extra == 'firebirdsql'
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
29
+ Requires-Dist: pytest>=7.0; extra == 'test'
30
+ Requires-Dist: testcontainers>=3.7.0; extra == 'test'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # sqlalchemy-firebird-async
34
+
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)
38
+
39
+ **Asynchronous Firebird dialect for SQLAlchemy.**
40
+
41
+ This library provides proper `asyncio` support for Firebird databases in SQLAlchemy 2.0+, allowing you to write fully asynchronous code using modern Python patterns.
42
+
43
+ It supports three underlying drivers:
44
+ 1. **`fdb`** (Legacy) - Runs the official C-based driver in a thread pool. Fast and stable.
45
+ 2. **`firebird-driver`** (Modern) - Official driver for Firebird 3+. Also threaded (run_in_executor).
46
+ 3. **`firebirdsql`** - Pure Python asyncio driver. Currently experimental due to upstream issues.
47
+
48
+ ## 📦 Installation
49
+
50
+ Install using pip:
51
+
52
+ ```bash
53
+ # Install with the FDB driver (Legacy, Stable)
54
+ pip install "sqlalchemy-firebird-async[fdb]"
55
+
56
+ # Install with the Firebird Driver (Modern, FB 3.0+)
57
+ pip install "sqlalchemy-firebird-async[firebird-driver]"
58
+
59
+ # Install with pure python driver (Experimental)
60
+ pip install "sqlalchemy-firebird-async[firebirdsql]"
61
+ # Note: For correct async behavior with firebirdsql, you might need a patched version:
62
+ # pip install git+https://github.com/attid/pyfirebirdsql.git
63
+ ```
64
+
65
+ ## 🚀 Quick Start
66
+
67
+ ### 1. Using FDB Driver (Legacy)
68
+
69
+ This dialect runs the legacy `fdb` driver in a thread pool.
70
+
71
+ **URL Scheme:** `firebird+fdb_async://`
72
+
73
+ ```python
74
+ import asyncio
75
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
76
+ from sqlalchemy import text
77
+
78
+ async def main():
79
+ # Format: firebird+fdb_async://user:password@host:port/path/to/db
80
+ dsn = "firebird+fdb_async://sysdba:masterkey@localhost:3050//firebird/data/employee.fdb"
81
+
82
+ engine = create_async_engine(dsn, echo=True)
83
+ # ... usage is identical for all drivers
84
+ ```
85
+
86
+ ### 2. Using Firebird Driver (Modern)
87
+
88
+ This dialect uses the modern `firebird-driver` package (the official driver for Firebird 3.0+), running in a thread pool. It requires Firebird 3.0 or higher.
89
+
90
+ **URL Scheme:** `firebird+firebird_async://`
91
+
92
+ ```python
93
+ engine = create_async_engine(
94
+ "firebird+firebird_async://sysdba:masterkey@localhost:3050//firebird/data/employee.fdb?charset=UTF8"
95
+ )
96
+ ```
97
+
98
+ ### 3. Using Native Async Driver (firebirdsql)
99
+
100
+ **Warning:** The upstream `firebirdsql` driver currently has issues with `asyncio` compatibility (bugs causing crashes or incorrect behavior).
101
+ A patched fork is available at [attid/pyfirebirdsql](https://github.com/attid/pyfirebirdsql.git), which fixes the async logic but currently exhibits significantly lower performance (approx. 4x slower than fdb).
102
+
103
+ **URL Scheme:** `firebird+firebirdsql_async://`
104
+
105
+ ```python
106
+ engine = create_async_engine(
107
+ "firebird+firebirdsql_async://sysdba:masterkey@localhost:3050//firebird/data/employee.fdb"
108
+ )
109
+ ```
110
+
111
+ ## 📊 Performance Comparison
112
+
113
+ We compared both drivers executing 5000 queries in 8 concurrent tasks (4 raw SQL + 4 ORM).
114
+
115
+ | Metric | **fdb (Threaded)** 🏆 | **firebirdsql (Patched)** | Difference |
116
+ | :--- | :--- | :--- | :--- |
117
+ | **Total Time** | **4.53s** | 116.20s | ~25x slower |
118
+ | **Avg Query Time (ORM)** | **2.54s** | 114.43s | ~45x slower |
119
+ | **Avg Query Time (Raw)** | **4.44s** | 116.14s | ~26x slower |
120
+ | **Parallel Ratio** | 6.16x | 7.94x | - |
121
+
122
+ *Benchmark details: 8 concurrent workers, 5000 rows each, total 40k rows.*
123
+
124
+ As seen above, `fdb` in a thread pool is significantly faster for high-load scenarios.
125
+
126
+ ## 🔌 Connection String Guide
127
+
128
+ | Driver | Protocol | URL Scheme |
129
+ | :--- | :--- | :--- |
130
+ | **fdb** (Legacy) | TCP/IP | `firebird+fdb_async://user:pass@host:port/db_path` |
131
+ | **firebird-driver** (Modern) | TCP/IP | `firebird+firebird_async://user:pass@host:port/db_path` |
132
+ | **firebirdsql** | TCP/IP | `firebird+firebirdsql_async://user:pass@host:port/db_path` |
133
+
134
+ ## 🤝 Contributing
135
+
136
+ Contributions are welcome! Please feel free to submit a Pull Request.
137
+
138
+ ## 📄 License
139
+
140
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,10 @@
1
+ sqlalchemy_firebird_async/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sqlalchemy_firebird_async/compiler.py,sha256=RWYdiL7Ep0PKXp_A2vs5FXHBsFU3WUexJiUZILwbsCw,582
3
+ sqlalchemy_firebird_async/fdb.py,sha256=Bp5YisZ4CmBE6eiULoWgiW588zQcmGursBL7nIqxHmI,5989
4
+ sqlalchemy_firebird_async/firebird_driver.py,sha256=KLsNCAkTKi04QV2v1_eMFuqJeY2pYSv___B1g6g2shQ,4457
5
+ sqlalchemy_firebird_async/firebirdsql.py,sha256=awoAHfcS7KtAnhbyzEypcJTL4ITnm_RoAh-t3EKiZvw,6530
6
+ sqlalchemy_firebird_async-0.2.1.dist-info/METADATA,sha256=mDMfOORLIRVzW-6yhbIZhPVMbJSET0Nh-BoBrSqkLyA,5261
7
+ sqlalchemy_firebird_async-0.2.1.dist-info/WHEEL,sha256=aha0VrrYvgDJ3Xxl3db_g_MDIW-ZexDdrc_m-Hk8YY4,105
8
+ sqlalchemy_firebird_async-0.2.1.dist-info/entry_points.txt,sha256=j3WOgJ-aftFHMnkrPTLxZj1tIEwdQql-xgGYNew8YXM,269
9
+ sqlalchemy_firebird_async-0.2.1.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
10
+ sqlalchemy_firebird_async-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [sqlalchemy.dialects]
2
+ firebird.fdb_async = sqlalchemy_firebird_async.fdb:AsyncFDBDialect
3
+ firebird.firebird_async = sqlalchemy_firebird_async.firebird_driver:AsyncFirebirdDialect
4
+ firebird.firebirdsql_async = sqlalchemy_firebird_async.firebirdsql:AsyncFirebirdSQLDialect
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.