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.
- sqlalchemy_firebird_async/__init__.py +0 -0
- sqlalchemy_firebird_async/compiler.py +12 -0
- sqlalchemy_firebird_async/fdb.py +160 -0
- sqlalchemy_firebird_async/firebird_driver.py +138 -0
- sqlalchemy_firebird_async/firebirdsql.py +190 -0
- sqlalchemy_firebird_async-0.2.1.dist-info/METADATA +140 -0
- sqlalchemy_firebird_async-0.2.1.dist-info/RECORD +10 -0
- sqlalchemy_firebird_async-0.2.1.dist-info/WHEEL +5 -0
- sqlalchemy_firebird_async-0.2.1.dist-info/entry_points.txt +4 -0
- sqlalchemy_firebird_async-0.2.1.dist-info/licenses/LICENSE +21 -0
|
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
|
+

|
|
36
|
+

|
|
37
|
+

|
|
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,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.
|