sqlalchemy-firebird-async 0.2.0__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-firebird-async
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Asyncio support for Firebird in SQLAlchemy
5
5
  Project-URL: Homepage, https://github.com/attid/sqlalchemy-firebird-async
6
6
  Author-email: Igor Tolstov <attid0@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlalchemy-firebird-async"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Asyncio support for Firebird in SQLAlchemy"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -72,6 +72,9 @@ class AsyncConnection:
72
72
 
73
73
  def close(self):
74
74
  return self._exec(self._sync_connection.close)
75
+
76
+ def terminate(self):
77
+ return self._exec(self._sync_connection.close)
75
78
 
76
79
  def __getattr__(self, name):
77
80
  return getattr(self._sync_connection, name)
@@ -73,6 +73,9 @@ class AsyncConnection:
73
73
 
74
74
  def close(self):
75
75
  return self._exec(self._sync_connection.close)
76
+
77
+ def terminate(self):
78
+ return self._exec(self._sync_connection.close)
76
79
 
77
80
  def __getattr__(self, name):
78
81
  return getattr(self._sync_connection, name)
@@ -0,0 +1,59 @@
1
+ import pytest
2
+ from sqlalchemy import text
3
+
4
+ @pytest.mark.asyncio
5
+ async def test_engine_terminate(async_engine):
6
+ """
7
+ Тест проверяет корректность закрытия соединений при dispose.
8
+ Это должно вызывать do_terminate -> terminate.
9
+ """
10
+ # 1. Открываем соединение
11
+ async with async_engine.connect() as conn:
12
+ await conn.execute(text("SELECT 1 FROM rdb$database"))
13
+ # Не закрываем явно (хотя контекст менеджер закроет)
14
+
15
+ # 2. Форсируем закрытие пула
16
+ # dispose() вызывает pool.dispose(), который закрывает все соединения.
17
+ # Если пул был QueuePool (стандартный), он может попытаться вызвать terminate
18
+ # для проверенных соединений, если они считаются "invalid" или при очистке.
19
+
20
+ # Чтобы гарантированно вызвать terminate, можно попробовать сымитировать ошибку
21
+ # или просто положиться на то, что пул вызывает terminate при сбросе.
22
+
23
+ await async_engine.dispose()
24
+
25
+ # 3. Прямой тест метода terminate на объекте соединения (низкоуровневый)
26
+ # Нам нужно добраться до raw connection
27
+ raw_conn = await async_engine.raw_connection()
28
+ try:
29
+ # В SQLAlchemy 2.0+ raw_connection возвращает адаптер
30
+ # Нам нужно вызвать метод terminate, который ожидает диалект
31
+ if hasattr(raw_conn, "terminate"):
32
+ raw_conn.terminate()
33
+ else:
34
+ # Если метода нет, это уже ошибка (AttributeError бы вылетел выше, если бы вызов шел)
35
+ # Но ошибка из стектрейса пользователя была в do_terminate диалекта
36
+
37
+ # Эмулируем то, что делает пул:
38
+ # dialect.do_terminate(dbapi_connection)
39
+ dialect = async_engine.dialect
40
+ # raw_conn это AsyncAdapt_dbapi_connection
41
+ # Внутри него .dbapi_connection это наш AsyncConnection
42
+
43
+ # Но terminate вызывается у dbapi_connection.
44
+ # Попробуем вызвать его у нашего AsyncConnection
45
+ real_conn = raw_conn.driver_connection
46
+
47
+ # Проверяем наличие метода, так как его отсутствие вызывает ошибку
48
+ # Если мы его не добавим, тут будет AttributeError (если вызывать)
49
+
50
+ # Эмуляция вызова из диалекта:
51
+ try:
52
+ dialect.do_terminate(real_conn)
53
+ except AttributeError as e:
54
+ if "'terminate'" in str(e):
55
+ pytest.fail(f"Missing terminate method: {e}")
56
+ else:
57
+ raise e
58
+ finally:
59
+ raw_conn.close()
@@ -0,0 +1,141 @@
1
+ import pytest
2
+ import decimal
3
+ import datetime
4
+ from sqlalchemy import text
5
+
6
+ @pytest.mark.asyncio
7
+ async def test_primitive_types(async_engine):
8
+ """Тест базовых типов: INT, BIGINT, FLOAT, BOOLEAN, VARCHAR, DECIMAL."""
9
+ # 1. DDL
10
+ async with async_engine.begin() as conn:
11
+ try: await conn.execute(text("DROP TABLE all_types"))
12
+ except Exception: pass
13
+
14
+ await conn.execute(text("""
15
+ CREATE TABLE all_types (
16
+ id INTEGER PRIMARY KEY,
17
+ f_integer INTEGER,
18
+ f_bigint BIGINT,
19
+ f_float FLOAT,
20
+ f_boolean BOOLEAN,
21
+ f_string VARCHAR(100),
22
+ f_decimal DECIMAL(10, 2)
23
+ )
24
+ """))
25
+
26
+ # 2. DML
27
+ test_data = {
28
+ "id": 1,
29
+ "f_integer": 42,
30
+ "f_bigint": 9223372036854775807,
31
+ "f_float": 3.14159,
32
+ "f_boolean": True,
33
+ "f_string": "Hello Firebird",
34
+ "f_decimal": decimal.Decimal("123.45")
35
+ }
36
+
37
+ async with async_engine.begin() as conn:
38
+ await conn.execute(
39
+ text("""
40
+ INSERT INTO all_types (id, f_integer, f_bigint, f_float, f_boolean, f_string, f_decimal)
41
+ VALUES (:id, :f_integer, :f_bigint, :f_float, :f_boolean, :f_string, :f_decimal)
42
+ """),
43
+ test_data
44
+ )
45
+
46
+ # 3. SELECT
47
+ async with async_engine.connect() as conn:
48
+ result = await conn.execute(text("SELECT * FROM all_types WHERE id = 1"))
49
+ row = result.fetchone()
50
+ assert row.f_integer == 42
51
+ assert row.f_bigint == 9223372036854775807
52
+ assert abs(row.f_float - 3.14159) < 0.00001
53
+ assert row.f_boolean is True
54
+ assert row.f_string == "Hello Firebird"
55
+ assert row.f_decimal == decimal.Decimal("123.45")
56
+
57
+ @pytest.mark.asyncio
58
+ async def test_complex_types_fb4(async_engine):
59
+ """
60
+ Тестирование сложных типов FB4 (INT128, Time Zones).
61
+ Пропускается для fdb_async, так как старый драйвер их не поддерживает.
62
+ """
63
+ if "fdb_async" in async_engine.url.drivername:
64
+ pytest.skip("fdb driver does not support INT128 or Time Zones")
65
+
66
+ now_tz = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
67
+ test_val_38 = decimal.Decimal("1234567890123456789012345678.9012345678")
68
+ test_blob = "Very large text blob " * 100
69
+
70
+ async with async_engine.begin() as conn:
71
+ try: await conn.execute(text("DROP TABLE complex_types"))
72
+ except Exception: pass
73
+ await conn.execute(text("""
74
+ CREATE TABLE complex_types (
75
+ id INTEGER PRIMARY KEY,
76
+ f_numeric_38 NUMERIC(38, 10),
77
+ f_timestamp_tz TIMESTAMP WITH TIME ZONE,
78
+ f_blob BLOB SUB_TYPE TEXT
79
+ )
80
+ """))
81
+
82
+ async with async_engine.begin() as conn:
83
+ await conn.execute(
84
+ text("""
85
+ INSERT INTO complex_types (id, f_numeric_38, f_timestamp_tz, f_blob)
86
+ VALUES (:id, :f_numeric_38, :f_timestamp_tz, :f_blob)
87
+ """),
88
+ {"id": 1, "f_numeric_38": test_val_38, "f_timestamp_tz": now_tz, "f_blob": test_blob}
89
+ )
90
+
91
+ async with async_engine.connect() as conn:
92
+ result = await conn.execute(text("SELECT * FROM complex_types WHERE id = 1"))
93
+ row = result.fetchone()
94
+ assert row.f_numeric_38 == test_val_38
95
+ assert isinstance(row.f_timestamp_tz, datetime.datetime)
96
+ assert abs(row.f_timestamp_tz.timestamp() - now_tz.timestamp()) < 1.0
97
+ assert row.f_blob == test_blob
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_aggregations(async_engine):
101
+ """Проверка агрегатных функций."""
102
+ if "fdb_async" in async_engine.url.drivername:
103
+ pytest.skip("fdb driver has issues with SQLDA for aggregations in async wrapper")
104
+
105
+ async with async_engine.begin() as conn:
106
+ try: await conn.execute(text("DROP TABLE test_agg"))
107
+ except Exception: pass
108
+ await conn.execute(text("CREATE TABLE test_agg (val DECIMAL(10,2))"))
109
+
110
+ async with async_engine.begin() as conn:
111
+ for v in [10.5, 20.5, 30.0]:
112
+ await conn.execute(text("INSERT INTO test_agg (val) VALUES (:v)"), {"v": v})
113
+
114
+ async with async_engine.connect() as conn:
115
+ res = await conn.execute(text("SELECT SUM(val), COUNT(*) FROM test_agg"))
116
+ s, c = res.fetchone()
117
+ assert s == decimal.Decimal("61.00")
118
+ assert c == 3
119
+
120
+ @pytest.mark.asyncio
121
+ async def test_insert_returning(async_engine):
122
+ """Проверка RETURNING."""
123
+ async with async_engine.begin() as conn:
124
+ try: await conn.execute(text("DROP TABLE test_ret"))
125
+ except Exception: pass
126
+ await conn.execute(text("CREATE TABLE test_ret (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(50))"))
127
+
128
+ async with async_engine.begin() as conn:
129
+ res = await conn.execute(
130
+ text("INSERT INTO test_ret (name) VALUES (:name) RETURNING id"),
131
+ {"name": "test"}
132
+ )
133
+ new_id = res.scalar()
134
+ assert isinstance(new_id, int)
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_statement_string_compilation(async_engine):
138
+ """Проверка компиляции запроса в строку (для print)."""
139
+ stmt = text("SELECT 1 FROM rdb$database WHERE 1 = :id").bindparams(id=1)
140
+ compiled_str = str(stmt.compile(async_engine.sync_engine))
141
+ assert "1 = " in compiled_str