matrixone-python-sdk 0.1.0__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.
- matrixone/__init__.py +155 -0
- matrixone/account.py +723 -0
- matrixone/async_client.py +3913 -0
- matrixone/async_metadata_manager.py +311 -0
- matrixone/async_orm.py +123 -0
- matrixone/async_vector_index_manager.py +633 -0
- matrixone/base_client.py +208 -0
- matrixone/client.py +4672 -0
- matrixone/config.py +452 -0
- matrixone/connection_hooks.py +286 -0
- matrixone/exceptions.py +89 -0
- matrixone/logger.py +782 -0
- matrixone/metadata.py +820 -0
- matrixone/moctl.py +219 -0
- matrixone/orm.py +2277 -0
- matrixone/pitr.py +646 -0
- matrixone/pubsub.py +771 -0
- matrixone/restore.py +411 -0
- matrixone/search_vector_index.py +1176 -0
- matrixone/snapshot.py +550 -0
- matrixone/sql_builder.py +844 -0
- matrixone/sqlalchemy_ext/__init__.py +161 -0
- matrixone/sqlalchemy_ext/adapters.py +163 -0
- matrixone/sqlalchemy_ext/dialect.py +534 -0
- matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
- matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
- matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
- matrixone/sqlalchemy_ext/ivf_config.py +252 -0
- matrixone/sqlalchemy_ext/table_builder.py +351 -0
- matrixone/sqlalchemy_ext/vector_index.py +1721 -0
- matrixone/sqlalchemy_ext/vector_type.py +948 -0
- matrixone/version.py +580 -0
- matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
- matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
- matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
- matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
- matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +19 -0
- tests/offline/__init__.py +20 -0
- tests/offline/conftest.py +77 -0
- tests/offline/test_account.py +703 -0
- tests/offline/test_async_client_query_comprehensive.py +1218 -0
- tests/offline/test_basic.py +54 -0
- tests/offline/test_case_sensitivity.py +227 -0
- tests/offline/test_connection_hooks_offline.py +287 -0
- tests/offline/test_dialect_schema_handling.py +609 -0
- tests/offline/test_explain_methods.py +346 -0
- tests/offline/test_filter_logical_in.py +237 -0
- tests/offline/test_fulltext_search_comprehensive.py +795 -0
- tests/offline/test_ivf_config.py +249 -0
- tests/offline/test_join_methods.py +281 -0
- tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
- tests/offline/test_logical_in_method.py +237 -0
- tests/offline/test_matrixone_version_parsing.py +264 -0
- tests/offline/test_metadata_offline.py +557 -0
- tests/offline/test_moctl.py +300 -0
- tests/offline/test_moctl_simple.py +251 -0
- tests/offline/test_model_support_offline.py +359 -0
- tests/offline/test_model_support_simple.py +225 -0
- tests/offline/test_pinecone_filter_offline.py +377 -0
- tests/offline/test_pitr.py +585 -0
- tests/offline/test_pubsub.py +712 -0
- tests/offline/test_query_update.py +283 -0
- tests/offline/test_restore.py +445 -0
- tests/offline/test_snapshot_comprehensive.py +384 -0
- tests/offline/test_sql_escaping_edge_cases.py +551 -0
- tests/offline/test_sqlalchemy_integration.py +382 -0
- tests/offline/test_sqlalchemy_vector_integration.py +434 -0
- tests/offline/test_table_builder.py +198 -0
- tests/offline/test_unified_filter.py +398 -0
- tests/offline/test_unified_transaction.py +495 -0
- tests/offline/test_vector_index.py +238 -0
- tests/offline/test_vector_operations.py +688 -0
- tests/offline/test_vector_type.py +174 -0
- tests/offline/test_version_core.py +328 -0
- tests/offline/test_version_management.py +372 -0
- tests/offline/test_version_standalone.py +652 -0
- tests/online/__init__.py +20 -0
- tests/online/conftest.py +216 -0
- tests/online/test_account_management.py +194 -0
- tests/online/test_advanced_features.py +344 -0
- tests/online/test_async_client_interfaces.py +330 -0
- tests/online/test_async_client_online.py +285 -0
- tests/online/test_async_model_insert_online.py +293 -0
- tests/online/test_async_orm_online.py +300 -0
- tests/online/test_async_simple_query_online.py +802 -0
- tests/online/test_async_transaction_simple_query.py +300 -0
- tests/online/test_basic_connection.py +130 -0
- tests/online/test_client_online.py +238 -0
- tests/online/test_config.py +90 -0
- tests/online/test_config_validation.py +123 -0
- tests/online/test_connection_hooks_new_online.py +217 -0
- tests/online/test_dialect_schema_handling_online.py +331 -0
- tests/online/test_filter_logical_in_online.py +374 -0
- tests/online/test_fulltext_comprehensive.py +1773 -0
- tests/online/test_fulltext_label_online.py +433 -0
- tests/online/test_fulltext_search_online.py +842 -0
- tests/online/test_ivf_stats_online.py +506 -0
- tests/online/test_logger_integration.py +311 -0
- tests/online/test_matrixone_query_orm.py +540 -0
- tests/online/test_metadata_online.py +579 -0
- tests/online/test_model_insert_online.py +255 -0
- tests/online/test_mysql_driver_validation.py +213 -0
- tests/online/test_orm_advanced_features.py +2022 -0
- tests/online/test_orm_cte_integration.py +269 -0
- tests/online/test_orm_online.py +270 -0
- tests/online/test_pinecone_filter.py +708 -0
- tests/online/test_pubsub_operations.py +352 -0
- tests/online/test_query_methods.py +225 -0
- tests/online/test_query_update_online.py +433 -0
- tests/online/test_search_vector_index.py +557 -0
- tests/online/test_simple_fulltext_online.py +915 -0
- tests/online/test_snapshot_comprehensive.py +998 -0
- tests/online/test_sqlalchemy_engine_integration.py +336 -0
- tests/online/test_sqlalchemy_integration.py +425 -0
- tests/online/test_transaction_contexts.py +1219 -0
- tests/online/test_transaction_insert_methods.py +356 -0
- tests/online/test_transaction_query_methods.py +288 -0
- tests/online/test_unified_filter_online.py +529 -0
- tests/online/test_vector_comprehensive.py +706 -0
- tests/online/test_version_management.py +291 -0
@@ -0,0 +1,1219 @@
|
|
1
|
+
# Copyright 2021 - 2022 Matrix Origin
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""
|
16
|
+
Test that main interfaces work in both transaction contexts:
|
17
|
+
1. MatrixOne Client transaction context: with client.transaction() as tx:
|
18
|
+
2. SQLAlchemy transaction context: with client.get_sqlalchemy_engine().begin() as conn:
|
19
|
+
"""
|
20
|
+
|
21
|
+
import pytest
|
22
|
+
import pytest_asyncio
|
23
|
+
from matrixone import Client, AsyncClient
|
24
|
+
from contextlib import contextmanager
|
25
|
+
from .test_config import online_config
|
26
|
+
from matrixone.sqlalchemy_ext import boolean_match
|
27
|
+
|
28
|
+
|
29
|
+
class TestSyncTransactionContexts:
|
30
|
+
"""Test sync client transaction contexts"""
|
31
|
+
|
32
|
+
@pytest.fixture(scope="function")
|
33
|
+
def sync_client_setup(self):
|
34
|
+
"""Setup sync client for testing"""
|
35
|
+
client = Client()
|
36
|
+
host, port, user, password, database = online_config.get_connection_params()
|
37
|
+
client.connect(host, port, user, password, database)
|
38
|
+
|
39
|
+
# Enable fulltext indexing
|
40
|
+
client.execute("SET experimental_fulltext_index=1")
|
41
|
+
|
42
|
+
# Create test database and table
|
43
|
+
test_db = "sync_tx_context_test"
|
44
|
+
client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
45
|
+
client.execute(f"USE {test_db}")
|
46
|
+
|
47
|
+
client.execute("DROP TABLE IF EXISTS sync_tx_docs")
|
48
|
+
client.execute(
|
49
|
+
"""
|
50
|
+
CREATE TABLE sync_tx_docs (
|
51
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
52
|
+
title VARCHAR(255) NOT NULL,
|
53
|
+
content TEXT NOT NULL,
|
54
|
+
category VARCHAR(100) NOT NULL
|
55
|
+
)
|
56
|
+
"""
|
57
|
+
)
|
58
|
+
|
59
|
+
# Insert test data
|
60
|
+
test_docs = [
|
61
|
+
("Python Programming", "Learn Python programming basics", "Programming"),
|
62
|
+
("JavaScript Development", "Modern JavaScript development patterns", "Programming"),
|
63
|
+
("Database Design", "Database design principles and best practices", "Database"),
|
64
|
+
("Web Development", "Web development with modern frameworks", "Web"),
|
65
|
+
("Testing Strategies", "Software testing strategies and methodologies", "Testing"),
|
66
|
+
]
|
67
|
+
|
68
|
+
for title, content, category in test_docs:
|
69
|
+
client.execute(
|
70
|
+
f"""
|
71
|
+
INSERT INTO sync_tx_docs (title, content, category)
|
72
|
+
VALUES ('{title}', '{content}', '{category}')
|
73
|
+
"""
|
74
|
+
)
|
75
|
+
|
76
|
+
# Create fulltext index
|
77
|
+
client.fulltext_index.create("sync_tx_docs", "ftidx_sync_tx", ["title", "content"])
|
78
|
+
|
79
|
+
yield client, test_db
|
80
|
+
|
81
|
+
# Cleanup
|
82
|
+
try:
|
83
|
+
client.fulltext_index.drop("sync_tx_docs", "ftidx_sync_tx")
|
84
|
+
client.execute("DROP TABLE sync_tx_docs")
|
85
|
+
client.execute(f"DROP DATABASE {test_db}")
|
86
|
+
except Exception as e:
|
87
|
+
print(f"Cleanup warning: {e}")
|
88
|
+
finally:
|
89
|
+
client.disconnect()
|
90
|
+
|
91
|
+
def test_sync_client_transaction_context(self, sync_client_setup):
|
92
|
+
"""Test sync client transaction context"""
|
93
|
+
client, test_db = sync_client_setup
|
94
|
+
|
95
|
+
with client.transaction() as tx:
|
96
|
+
# Test basic execute
|
97
|
+
result = tx.execute("SELECT COUNT(*) FROM sync_tx_docs")
|
98
|
+
assert result.rows[0][0] == 5
|
99
|
+
|
100
|
+
# Test fulltext search
|
101
|
+
result = tx.query(
|
102
|
+
"sync_tx_docs.title", "sync_tx_docs.content", boolean_match("title", "content").encourage("python")
|
103
|
+
).execute()
|
104
|
+
assert len(result.rows) > 0
|
105
|
+
|
106
|
+
# Test snapshot operations (commented out as MatrixOne doesn't support snapshots in transactions)
|
107
|
+
# tx.snapshots.create("test_snap", "table", database=test_db, table="sync_tx_docs")
|
108
|
+
|
109
|
+
# Test SQLAlchemy session
|
110
|
+
session = tx.get_sqlalchemy_session()
|
111
|
+
assert session is not None
|
112
|
+
|
113
|
+
# Test get_connection for direct connection access
|
114
|
+
conn = tx.get_connection()
|
115
|
+
assert conn is not None
|
116
|
+
|
117
|
+
# Test direct connection usage
|
118
|
+
from sqlalchemy import text
|
119
|
+
|
120
|
+
result = conn.execute(text("SELECT COUNT(*) FROM sync_tx_docs"))
|
121
|
+
rows = result.fetchall()
|
122
|
+
assert rows[0][0] == 5
|
123
|
+
|
124
|
+
# Test account operations
|
125
|
+
# Note: Account operations might require special permissions
|
126
|
+
# tx.account.create_user("test_user", "test_password")
|
127
|
+
|
128
|
+
def test_sync_sqlalchemy_transaction_context(self, sync_client_setup):
|
129
|
+
"""Test sync SQLAlchemy transaction context"""
|
130
|
+
client, test_db = sync_client_setup
|
131
|
+
|
132
|
+
with client.get_sqlalchemy_engine().begin() as conn:
|
133
|
+
# Test direct SQL execution
|
134
|
+
from sqlalchemy import text
|
135
|
+
|
136
|
+
result = conn.execute(text("SELECT COUNT(*) FROM sync_tx_docs"))
|
137
|
+
rows = result.fetchall()
|
138
|
+
assert rows[0][0] == 5
|
139
|
+
|
140
|
+
# Test SQLAlchemy session in transaction
|
141
|
+
from sqlalchemy.orm import sessionmaker
|
142
|
+
|
143
|
+
Session = sessionmaker(bind=conn)
|
144
|
+
session = Session()
|
145
|
+
assert session is not None
|
146
|
+
session.close()
|
147
|
+
|
148
|
+
|
149
|
+
class TestAsyncTransactionContexts:
|
150
|
+
"""Test async client transaction contexts"""
|
151
|
+
|
152
|
+
@pytest_asyncio.fixture(scope="function")
|
153
|
+
async def async_client_setup(self):
|
154
|
+
"""Setup async client for testing"""
|
155
|
+
client = AsyncClient()
|
156
|
+
host, port, user, password, database = online_config.get_connection_params()
|
157
|
+
await client.connect(host, port, user, password, database)
|
158
|
+
|
159
|
+
# Enable fulltext indexing
|
160
|
+
await client.execute("SET experimental_fulltext_index=1")
|
161
|
+
|
162
|
+
# Create test database and table
|
163
|
+
test_db = "async_tx_context_test"
|
164
|
+
await client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
165
|
+
await client.execute(f"USE {test_db}")
|
166
|
+
|
167
|
+
await client.execute("DROP TABLE IF EXISTS async_tx_docs")
|
168
|
+
await client.execute(
|
169
|
+
"""
|
170
|
+
CREATE TABLE async_tx_docs (
|
171
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
172
|
+
title VARCHAR(255) NOT NULL,
|
173
|
+
content TEXT NOT NULL,
|
174
|
+
category VARCHAR(100) NOT NULL
|
175
|
+
)
|
176
|
+
"""
|
177
|
+
)
|
178
|
+
|
179
|
+
# Insert test data
|
180
|
+
test_docs = [
|
181
|
+
(
|
182
|
+
"Python Async Programming",
|
183
|
+
"Learn Python async programming with asyncio",
|
184
|
+
"Programming",
|
185
|
+
),
|
186
|
+
(
|
187
|
+
"JavaScript Async Patterns",
|
188
|
+
"Modern JavaScript async patterns and promises",
|
189
|
+
"Programming",
|
190
|
+
),
|
191
|
+
(
|
192
|
+
"Database Async Operations",
|
193
|
+
"Async database operations and connection pooling",
|
194
|
+
"Database",
|
195
|
+
),
|
196
|
+
("Web Async Development", "Async web development with modern frameworks", "Web"),
|
197
|
+
(
|
198
|
+
"Async Testing Strategies",
|
199
|
+
"Testing async code and handling async test cases",
|
200
|
+
"Testing",
|
201
|
+
),
|
202
|
+
]
|
203
|
+
|
204
|
+
for title, content, category in test_docs:
|
205
|
+
await client.execute(
|
206
|
+
f"""
|
207
|
+
INSERT INTO async_tx_docs (title, content, category)
|
208
|
+
VALUES ('{title}', '{content}', '{category}')
|
209
|
+
"""
|
210
|
+
)
|
211
|
+
|
212
|
+
# Create fulltext index
|
213
|
+
await client.fulltext_index.create("async_tx_docs", "ftidx_async_tx", ["title", "content"])
|
214
|
+
|
215
|
+
yield client, test_db
|
216
|
+
|
217
|
+
# Cleanup
|
218
|
+
try:
|
219
|
+
await client.fulltext_index.drop("async_tx_docs", "ftidx_async_tx")
|
220
|
+
await client.execute("DROP TABLE async_tx_docs")
|
221
|
+
await client.execute(f"DROP DATABASE {test_db}")
|
222
|
+
except Exception as e:
|
223
|
+
print(f"Cleanup warning: {e}")
|
224
|
+
finally:
|
225
|
+
await client.disconnect()
|
226
|
+
|
227
|
+
@pytest.mark.asyncio
|
228
|
+
async def test_async_client_transaction_context(self, async_client_setup):
|
229
|
+
"""Test async client transaction context"""
|
230
|
+
client, test_db = async_client_setup
|
231
|
+
|
232
|
+
async with client.transaction() as tx:
|
233
|
+
# Test basic execute
|
234
|
+
result = await tx.execute("SELECT COUNT(*) FROM async_tx_docs")
|
235
|
+
assert result.rows[0][0] == 5
|
236
|
+
|
237
|
+
# Test fulltext search
|
238
|
+
result = await tx.query(
|
239
|
+
"async_tx_docs.title", "async_tx_docs.content", boolean_match("title", "content").encourage("python")
|
240
|
+
).execute()
|
241
|
+
assert len(result.rows) > 0
|
242
|
+
|
243
|
+
# Test snapshot operations (commented out as MatrixOne doesn't support snapshots in transactions)
|
244
|
+
# await tx.snapshots.create("test_snap", "table", database=test_db, table="async_tx_docs")
|
245
|
+
|
246
|
+
# Test SQLAlchemy session
|
247
|
+
session = await tx.get_sqlalchemy_session()
|
248
|
+
assert session is not None
|
249
|
+
|
250
|
+
# Test get_connection for direct connection access
|
251
|
+
conn = tx.get_connection()
|
252
|
+
assert conn is not None
|
253
|
+
|
254
|
+
# Test direct connection usage
|
255
|
+
from sqlalchemy import text
|
256
|
+
|
257
|
+
result = await conn.execute(text("SELECT COUNT(*) FROM async_tx_docs"))
|
258
|
+
rows = result.fetchall()
|
259
|
+
assert rows[0][0] == 5
|
260
|
+
|
261
|
+
@pytest.mark.asyncio
|
262
|
+
async def test_async_sqlalchemy_transaction_context(self, async_client_setup):
|
263
|
+
"""Test async SQLAlchemy transaction context"""
|
264
|
+
client, test_db = async_client_setup
|
265
|
+
|
266
|
+
async with client.get_sqlalchemy_engine().begin() as conn:
|
267
|
+
# Test direct SQL execution
|
268
|
+
from sqlalchemy import text
|
269
|
+
|
270
|
+
result = await conn.execute(text("SELECT COUNT(*) FROM async_tx_docs"))
|
271
|
+
rows = result.fetchall()
|
272
|
+
assert rows[0][0] == 5
|
273
|
+
|
274
|
+
# Test SQLAlchemy session in transaction
|
275
|
+
try:
|
276
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
277
|
+
|
278
|
+
AsyncSessionLocal = async_sessionmaker(bind=conn, class_=AsyncSession, expire_on_commit=False)
|
279
|
+
except ImportError:
|
280
|
+
# Fallback for older SQLAlchemy versions
|
281
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
282
|
+
from sqlalchemy.orm import sessionmaker
|
283
|
+
|
284
|
+
AsyncSessionLocal = sessionmaker(bind=conn, class_=AsyncSession, expire_on_commit=False)
|
285
|
+
session = AsyncSessionLocal()
|
286
|
+
assert session is not None
|
287
|
+
await session.close()
|
288
|
+
|
289
|
+
|
290
|
+
class TestTransactionContextCompatibility:
|
291
|
+
"""Test compatibility between different transaction contexts"""
|
292
|
+
|
293
|
+
@pytest.fixture(scope="function")
|
294
|
+
def sync_client_setup(self):
|
295
|
+
"""Setup sync client for testing"""
|
296
|
+
client = Client()
|
297
|
+
host, port, user, password, database = online_config.get_connection_params()
|
298
|
+
client.connect(host, port, user, password, database)
|
299
|
+
|
300
|
+
# Enable fulltext indexing
|
301
|
+
client.execute("SET experimental_fulltext_index=1")
|
302
|
+
|
303
|
+
# Create test database and table
|
304
|
+
test_db = "compat_tx_test"
|
305
|
+
client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
306
|
+
client.execute(f"USE {test_db}")
|
307
|
+
|
308
|
+
client.execute("DROP TABLE IF EXISTS compat_tx_docs")
|
309
|
+
client.execute(
|
310
|
+
"""
|
311
|
+
CREATE TABLE compat_tx_docs (
|
312
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
313
|
+
title VARCHAR(255) NOT NULL,
|
314
|
+
content TEXT NOT NULL
|
315
|
+
)
|
316
|
+
"""
|
317
|
+
)
|
318
|
+
|
319
|
+
# Insert test data
|
320
|
+
client.execute("INSERT INTO compat_tx_docs (title, content) VALUES ('Test Title', 'Test Content')")
|
321
|
+
|
322
|
+
# Create fulltext index
|
323
|
+
client.fulltext_index.create("compat_tx_docs", "ftidx_compat", ["title", "content"])
|
324
|
+
|
325
|
+
yield client, test_db
|
326
|
+
|
327
|
+
# Cleanup
|
328
|
+
try:
|
329
|
+
client.fulltext_index.drop("compat_tx_docs", "ftidx_compat")
|
330
|
+
client.execute("DROP TABLE compat_tx_docs")
|
331
|
+
client.execute(f"DROP DATABASE {test_db}")
|
332
|
+
except Exception as e:
|
333
|
+
print(f"Cleanup warning: {e}")
|
334
|
+
finally:
|
335
|
+
client.disconnect()
|
336
|
+
|
337
|
+
def test_sync_transaction_wrapper_has_all_managers(self, sync_client_setup):
|
338
|
+
"""Test that sync transaction wrapper has all necessary managers"""
|
339
|
+
client, test_db = sync_client_setup
|
340
|
+
|
341
|
+
with client.transaction() as tx:
|
342
|
+
# Check that all managers are available
|
343
|
+
assert hasattr(tx, 'execute')
|
344
|
+
assert hasattr(tx, 'snapshots')
|
345
|
+
assert hasattr(tx, 'clone')
|
346
|
+
assert hasattr(tx, 'restore')
|
347
|
+
assert hasattr(tx, 'pitr')
|
348
|
+
assert hasattr(tx, 'pubsub')
|
349
|
+
assert hasattr(tx, 'account')
|
350
|
+
assert hasattr(tx, 'vector_ops')
|
351
|
+
assert hasattr(tx, 'fulltext_index')
|
352
|
+
assert hasattr(tx, 'get_sqlalchemy_session')
|
353
|
+
|
354
|
+
# Test that fulltext_index has create and drop methods
|
355
|
+
assert hasattr(tx.fulltext_index, 'create')
|
356
|
+
assert hasattr(tx.fulltext_index, 'drop')
|
357
|
+
|
358
|
+
# Test that query method works in transaction context
|
359
|
+
result = tx.query("compat_tx_docs.title", "compat_tx_docs.content").execute()
|
360
|
+
assert len(result.rows) > 0
|
361
|
+
|
362
|
+
@pytest.mark.asyncio
|
363
|
+
async def test_async_transaction_wrapper_has_all_managers(self):
|
364
|
+
"""Test that async transaction wrapper has all necessary managers"""
|
365
|
+
client = AsyncClient()
|
366
|
+
host, port, user, password, database = online_config.get_connection_params()
|
367
|
+
await client.connect(host, port, user, password, database)
|
368
|
+
|
369
|
+
try:
|
370
|
+
# Enable fulltext indexing
|
371
|
+
await client.execute("SET experimental_fulltext_index=1")
|
372
|
+
|
373
|
+
# Create test database and table
|
374
|
+
test_db = "async_compat_tx_test"
|
375
|
+
await client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
376
|
+
await client.execute(f"USE {test_db}")
|
377
|
+
|
378
|
+
await client.execute("DROP TABLE IF EXISTS async_compat_tx_docs")
|
379
|
+
await client.execute(
|
380
|
+
"""
|
381
|
+
CREATE TABLE async_compat_tx_docs (
|
382
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
383
|
+
title VARCHAR(255) NOT NULL,
|
384
|
+
content TEXT NOT NULL
|
385
|
+
)
|
386
|
+
"""
|
387
|
+
)
|
388
|
+
|
389
|
+
# Insert test data
|
390
|
+
await client.execute("INSERT INTO async_compat_tx_docs (title, content) VALUES ('Test Title', 'Test Content')")
|
391
|
+
|
392
|
+
# Create fulltext index
|
393
|
+
await client.fulltext_index.create("async_compat_tx_docs", "ftidx_async_compat", ["title", "content"])
|
394
|
+
|
395
|
+
async with client.transaction() as tx:
|
396
|
+
# Check that all managers are available
|
397
|
+
assert hasattr(tx, 'execute')
|
398
|
+
assert hasattr(tx, 'snapshots')
|
399
|
+
assert hasattr(tx, 'clone')
|
400
|
+
assert hasattr(tx, 'restore')
|
401
|
+
assert hasattr(tx, 'pitr')
|
402
|
+
assert hasattr(tx, 'pubsub')
|
403
|
+
assert hasattr(tx, 'account')
|
404
|
+
assert hasattr(tx, 'fulltext_index')
|
405
|
+
assert hasattr(tx, 'get_sqlalchemy_session')
|
406
|
+
|
407
|
+
# Test that fulltext_index has create and drop methods
|
408
|
+
assert hasattr(tx.fulltext_index, 'create')
|
409
|
+
assert hasattr(tx.fulltext_index, 'drop')
|
410
|
+
|
411
|
+
# Test that query method works in transaction context
|
412
|
+
result = await tx.query("async_compat_tx_docs.title", "async_compat_tx_docs.content").execute()
|
413
|
+
assert len(result.rows) > 0
|
414
|
+
|
415
|
+
finally:
|
416
|
+
# Cleanup
|
417
|
+
try:
|
418
|
+
await client.fulltext_index.drop("async_compat_tx_docs", "ftidx_async_compat")
|
419
|
+
await client.execute("DROP TABLE async_compat_tx_docs")
|
420
|
+
await client.execute(f"DROP DATABASE {test_db}")
|
421
|
+
except Exception as e:
|
422
|
+
print(f"Cleanup warning: {e}")
|
423
|
+
finally:
|
424
|
+
await client.disconnect()
|
425
|
+
|
426
|
+
|
427
|
+
class TestTransactionContextFeatures:
|
428
|
+
"""Test specific features in transaction contexts"""
|
429
|
+
|
430
|
+
@pytest.fixture(scope="function")
|
431
|
+
def sync_client_setup(self):
|
432
|
+
"""Setup sync client for testing"""
|
433
|
+
client = Client()
|
434
|
+
host, port, user, password, database = online_config.get_connection_params()
|
435
|
+
client.connect(host, port, user, password, database)
|
436
|
+
|
437
|
+
# Enable fulltext indexing
|
438
|
+
client.execute("SET experimental_fulltext_index=1")
|
439
|
+
|
440
|
+
# Create test database and table
|
441
|
+
test_db = "features_tx_test"
|
442
|
+
client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
443
|
+
client.execute(f"USE {test_db}")
|
444
|
+
|
445
|
+
client.execute("DROP TABLE IF EXISTS features_tx_docs")
|
446
|
+
client.execute(
|
447
|
+
"""
|
448
|
+
CREATE TABLE features_tx_docs (
|
449
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
450
|
+
title VARCHAR(255) NOT NULL,
|
451
|
+
content TEXT NOT NULL,
|
452
|
+
category VARCHAR(100) NOT NULL
|
453
|
+
)
|
454
|
+
"""
|
455
|
+
)
|
456
|
+
|
457
|
+
# Insert test data
|
458
|
+
test_docs = [
|
459
|
+
("Feature Test 1", "Content for feature test 1", "Category1"),
|
460
|
+
("Feature Test 2", "Content for feature test 2", "Category2"),
|
461
|
+
("Feature Test 3", "Content for feature test 3", "Category1"),
|
462
|
+
]
|
463
|
+
|
464
|
+
for title, content, category in test_docs:
|
465
|
+
client.execute(
|
466
|
+
f"""
|
467
|
+
INSERT INTO features_tx_docs (title, content, category)
|
468
|
+
VALUES ('{title}', '{content}', '{category}')
|
469
|
+
"""
|
470
|
+
)
|
471
|
+
|
472
|
+
# Create fulltext index
|
473
|
+
client.fulltext_index.create("features_tx_docs", "ftidx_features", ["title", "content"])
|
474
|
+
|
475
|
+
yield client, test_db
|
476
|
+
|
477
|
+
# Cleanup
|
478
|
+
try:
|
479
|
+
client.fulltext_index.drop("features_tx_docs", "ftidx_features")
|
480
|
+
client.execute("DROP TABLE features_tx_docs")
|
481
|
+
client.execute(f"DROP DATABASE {test_db}")
|
482
|
+
except Exception as e:
|
483
|
+
print(f"Cleanup warning: {e}")
|
484
|
+
finally:
|
485
|
+
client.disconnect()
|
486
|
+
|
487
|
+
def test_sync_fulltext_features_in_transaction(self, sync_client_setup):
|
488
|
+
"""Test fulltext features in sync transaction context"""
|
489
|
+
client, test_db = sync_client_setup
|
490
|
+
|
491
|
+
with client.transaction() as tx:
|
492
|
+
# Test basic search
|
493
|
+
result = tx.query(
|
494
|
+
"features_tx_docs.title", "features_tx_docs.content", boolean_match("title", "content").encourage("feature")
|
495
|
+
).execute()
|
496
|
+
assert len(result.rows) > 0
|
497
|
+
|
498
|
+
# Test with score
|
499
|
+
result = tx.query(
|
500
|
+
"features_tx_docs.title",
|
501
|
+
"features_tx_docs.content",
|
502
|
+
boolean_match("title", "content").encourage("test").label("score"),
|
503
|
+
).execute()
|
504
|
+
assert len(result.rows) > 0
|
505
|
+
# Check that score column is present
|
506
|
+
assert len(result.columns) > 2 # title, content, score
|
507
|
+
|
508
|
+
# Test boolean mode
|
509
|
+
result = tx.query(
|
510
|
+
"features_tx_docs.title", "features_tx_docs.content", boolean_match("title", "content").must("feature")
|
511
|
+
).execute()
|
512
|
+
assert len(result.rows) > 0
|
513
|
+
|
514
|
+
# Test with WHERE conditions
|
515
|
+
result = (
|
516
|
+
tx.query("features_tx_docs.title", "features_tx_docs.content")
|
517
|
+
.filter(boolean_match("title", "content").encourage("test"), "features_tx_docs.category = 'Category1'")
|
518
|
+
.execute()
|
519
|
+
)
|
520
|
+
assert len(result.rows) > 0
|
521
|
+
|
522
|
+
# Test ordering and limit
|
523
|
+
result = (
|
524
|
+
tx.query(
|
525
|
+
"features_tx_docs.title",
|
526
|
+
"features_tx_docs.content",
|
527
|
+
boolean_match("title", "content").encourage("test").label("score"),
|
528
|
+
)
|
529
|
+
.order_by("score ASC")
|
530
|
+
.limit(2)
|
531
|
+
.execute()
|
532
|
+
)
|
533
|
+
assert len(result.rows) <= 2
|
534
|
+
|
535
|
+
@pytest.mark.asyncio
|
536
|
+
async def test_async_fulltext_features_in_transaction(self):
|
537
|
+
"""Test fulltext features in async transaction context"""
|
538
|
+
client = AsyncClient()
|
539
|
+
host, port, user, password, database = online_config.get_connection_params()
|
540
|
+
await client.connect(host, port, user, password, database)
|
541
|
+
|
542
|
+
try:
|
543
|
+
# Enable fulltext indexing
|
544
|
+
await client.execute("SET experimental_fulltext_index=1")
|
545
|
+
|
546
|
+
# Create test database and table
|
547
|
+
test_db = "async_features_tx_test"
|
548
|
+
await client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
549
|
+
await client.execute(f"USE {test_db}")
|
550
|
+
|
551
|
+
await client.execute("DROP TABLE IF EXISTS async_features_tx_docs")
|
552
|
+
await client.execute(
|
553
|
+
"""
|
554
|
+
CREATE TABLE async_features_tx_docs (
|
555
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
556
|
+
title VARCHAR(255) NOT NULL,
|
557
|
+
content TEXT NOT NULL,
|
558
|
+
category VARCHAR(100) NOT NULL
|
559
|
+
)
|
560
|
+
"""
|
561
|
+
)
|
562
|
+
|
563
|
+
# Insert test data
|
564
|
+
test_docs = [
|
565
|
+
("Async Feature Test 1", "Content for async feature test 1", "Category1"),
|
566
|
+
("Async Feature Test 2", "Content for async feature test 2", "Category2"),
|
567
|
+
("Async Feature Test 3", "Content for async feature test 3", "Category1"),
|
568
|
+
]
|
569
|
+
|
570
|
+
for title, content, category in test_docs:
|
571
|
+
await client.execute(
|
572
|
+
f"""
|
573
|
+
INSERT INTO async_features_tx_docs (title, content, category)
|
574
|
+
VALUES ('{title}', '{content}', '{category}')
|
575
|
+
"""
|
576
|
+
)
|
577
|
+
|
578
|
+
# Create fulltext index
|
579
|
+
await client.fulltext_index.create("async_features_tx_docs", "ftidx_async_features", ["title", "content"])
|
580
|
+
|
581
|
+
async with client.transaction() as tx:
|
582
|
+
# Test basic search
|
583
|
+
result = await tx.query(
|
584
|
+
"async_features_tx_docs.title",
|
585
|
+
"async_features_tx_docs.content",
|
586
|
+
boolean_match("title", "content").encourage("async"),
|
587
|
+
).execute()
|
588
|
+
assert len(result.rows) > 0
|
589
|
+
|
590
|
+
# Test with score
|
591
|
+
result = await tx.query(
|
592
|
+
"async_features_tx_docs.title",
|
593
|
+
"async_features_tx_docs.content",
|
594
|
+
boolean_match("title", "content").encourage("test").label("score"),
|
595
|
+
).execute()
|
596
|
+
assert len(result.rows) > 0
|
597
|
+
# Check that score column is present
|
598
|
+
assert len(result.columns) > 2 # title, content, score
|
599
|
+
|
600
|
+
# Test boolean mode
|
601
|
+
result = await tx.query(
|
602
|
+
"async_features_tx_docs.title",
|
603
|
+
"async_features_tx_docs.content",
|
604
|
+
boolean_match("title", "content").must("async"),
|
605
|
+
).execute()
|
606
|
+
assert len(result.rows) > 0
|
607
|
+
|
608
|
+
# Test with WHERE conditions
|
609
|
+
result = (
|
610
|
+
await tx.query("async_features_tx_docs.title", "async_features_tx_docs.content")
|
611
|
+
.filter(
|
612
|
+
boolean_match("title", "content").encourage("test"), "async_features_tx_docs.category = 'Category1'"
|
613
|
+
)
|
614
|
+
.execute()
|
615
|
+
)
|
616
|
+
assert len(result.rows) > 0
|
617
|
+
|
618
|
+
# Test ordering and limit
|
619
|
+
result = (
|
620
|
+
await tx.query(
|
621
|
+
"async_features_tx_docs.title",
|
622
|
+
"async_features_tx_docs.content",
|
623
|
+
boolean_match("title", "content").encourage("test").label("score"),
|
624
|
+
)
|
625
|
+
.order_by("score ASC")
|
626
|
+
.limit(2)
|
627
|
+
.execute()
|
628
|
+
)
|
629
|
+
assert len(result.rows) <= 2
|
630
|
+
|
631
|
+
finally:
|
632
|
+
# Cleanup
|
633
|
+
try:
|
634
|
+
await client.fulltext_index.drop("async_features_tx_docs", "ftidx_async_features")
|
635
|
+
await client.execute("DROP TABLE async_features_tx_docs")
|
636
|
+
await client.execute(f"DROP DATABASE {test_db}")
|
637
|
+
except Exception as e:
|
638
|
+
print(f"Cleanup warning: {e}")
|
639
|
+
finally:
|
640
|
+
await client.disconnect()
|
641
|
+
|
642
|
+
|
643
|
+
class TestGetConnectionInterface:
|
644
|
+
"""Test get_connection interface in transaction contexts"""
|
645
|
+
|
646
|
+
@pytest.fixture(scope="function")
|
647
|
+
def sync_client_setup(self):
|
648
|
+
"""Setup sync client for testing"""
|
649
|
+
client = Client()
|
650
|
+
host, port, user, password, database = online_config.get_connection_params()
|
651
|
+
client.connect(host, port, user, password, database)
|
652
|
+
|
653
|
+
# Create test database and table
|
654
|
+
test_db = "get_conn_test"
|
655
|
+
client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
656
|
+
client.execute(f"USE {test_db}")
|
657
|
+
|
658
|
+
client.execute("DROP TABLE IF EXISTS get_conn_docs")
|
659
|
+
client.execute(
|
660
|
+
"""
|
661
|
+
CREATE TABLE get_conn_docs (
|
662
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
663
|
+
title VARCHAR(255) NOT NULL,
|
664
|
+
content TEXT NOT NULL,
|
665
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
666
|
+
)
|
667
|
+
"""
|
668
|
+
)
|
669
|
+
|
670
|
+
# Insert test data
|
671
|
+
test_docs = [
|
672
|
+
("Connection Test 1", "Content for connection test 1"),
|
673
|
+
("Connection Test 2", "Content for connection test 2"),
|
674
|
+
("Connection Test 3", "Content for connection test 3"),
|
675
|
+
]
|
676
|
+
|
677
|
+
for title, content in test_docs:
|
678
|
+
client.execute(
|
679
|
+
f"""
|
680
|
+
INSERT INTO get_conn_docs (title, content)
|
681
|
+
VALUES ('{title}', '{content}')
|
682
|
+
"""
|
683
|
+
)
|
684
|
+
|
685
|
+
yield client, test_db
|
686
|
+
|
687
|
+
# Cleanup
|
688
|
+
try:
|
689
|
+
client.execute("DROP TABLE get_conn_docs")
|
690
|
+
client.execute(f"DROP DATABASE {test_db}")
|
691
|
+
except Exception as e:
|
692
|
+
print(f"Cleanup warning: {e}")
|
693
|
+
finally:
|
694
|
+
client.disconnect()
|
695
|
+
|
696
|
+
def test_sync_get_connection_basic_usage(self, sync_client_setup):
|
697
|
+
"""Test basic usage of get_connection in sync transaction"""
|
698
|
+
client, test_db = sync_client_setup
|
699
|
+
|
700
|
+
with client.transaction() as tx:
|
701
|
+
# Get connection
|
702
|
+
conn = tx.get_connection()
|
703
|
+
assert conn is not None
|
704
|
+
|
705
|
+
# Test basic query execution
|
706
|
+
from sqlalchemy import text
|
707
|
+
|
708
|
+
result = conn.execute(text(f"SELECT COUNT(*) FROM {test_db}.get_conn_docs"))
|
709
|
+
rows = result.fetchall()
|
710
|
+
assert rows[0][0] == 3
|
711
|
+
|
712
|
+
# Test parameterized query
|
713
|
+
result = conn.execute(
|
714
|
+
text(f"SELECT * FROM {test_db}.get_conn_docs WHERE title LIKE :pattern"),
|
715
|
+
{"pattern": "%Test 1%"},
|
716
|
+
)
|
717
|
+
rows = result.fetchall()
|
718
|
+
assert len(rows) == 1
|
719
|
+
assert "Test 1" in rows[0][1] # title column
|
720
|
+
|
721
|
+
def test_sync_get_connection_transaction_isolation(self, sync_client_setup):
|
722
|
+
"""Test that get_connection maintains transaction isolation"""
|
723
|
+
client, test_db = sync_client_setup
|
724
|
+
|
725
|
+
# First, verify initial state
|
726
|
+
result = client.execute(f"SELECT COUNT(*) FROM {test_db}.get_conn_docs")
|
727
|
+
initial_count = result.rows[0][0]
|
728
|
+
|
729
|
+
with client.transaction() as tx:
|
730
|
+
conn = tx.get_connection()
|
731
|
+
|
732
|
+
# Insert a new record within transaction
|
733
|
+
from sqlalchemy import text
|
734
|
+
|
735
|
+
conn.execute(
|
736
|
+
text(
|
737
|
+
f"INSERT INTO {test_db}.get_conn_docs (title, content) VALUES ('Transaction Test', 'Content in transaction')"
|
738
|
+
)
|
739
|
+
)
|
740
|
+
|
741
|
+
# Verify record exists within transaction
|
742
|
+
result = conn.execute(text(f"SELECT COUNT(*) FROM {test_db}.get_conn_docs"))
|
743
|
+
count_in_tx = result.scalar()
|
744
|
+
assert count_in_tx == initial_count + 1
|
745
|
+
|
746
|
+
# Verify record doesn't exist outside transaction (from another connection)
|
747
|
+
result = client.execute(f"SELECT COUNT(*) FROM {test_db}.get_conn_docs")
|
748
|
+
count_outside = result.rows[0][0]
|
749
|
+
assert count_outside == initial_count # Should still be original count
|
750
|
+
|
751
|
+
# After transaction commit, verify record exists
|
752
|
+
result = client.execute(f"SELECT COUNT(*) FROM {test_db}.get_conn_docs")
|
753
|
+
final_count = result.rows[0][0]
|
754
|
+
assert final_count == initial_count + 1
|
755
|
+
|
756
|
+
@pytest.mark.asyncio
|
757
|
+
async def test_async_get_connection_basic_usage(self):
|
758
|
+
"""Test basic usage of get_connection in async transaction"""
|
759
|
+
client = AsyncClient()
|
760
|
+
host, port, user, password, database = online_config.get_connection_params()
|
761
|
+
await client.connect(host, port, user, password, database)
|
762
|
+
|
763
|
+
try:
|
764
|
+
# Create test database and table
|
765
|
+
test_db = "async_get_conn_test"
|
766
|
+
await client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
767
|
+
await client.execute(f"USE {test_db}")
|
768
|
+
|
769
|
+
await client.execute("DROP TABLE IF EXISTS async_get_conn_docs")
|
770
|
+
await client.execute(
|
771
|
+
"""
|
772
|
+
CREATE TABLE async_get_conn_docs (
|
773
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
774
|
+
title VARCHAR(255) NOT NULL,
|
775
|
+
content TEXT NOT NULL,
|
776
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
777
|
+
)
|
778
|
+
"""
|
779
|
+
)
|
780
|
+
|
781
|
+
# Insert test data
|
782
|
+
test_docs = [
|
783
|
+
("Async Connection Test 1", "Content for async connection test 1"),
|
784
|
+
("Async Connection Test 2", "Content for async connection test 2"),
|
785
|
+
("Async Connection Test 3", "Content for async connection test 3"),
|
786
|
+
]
|
787
|
+
|
788
|
+
for title, content in test_docs:
|
789
|
+
await client.execute(
|
790
|
+
f"""
|
791
|
+
INSERT INTO async_get_conn_docs (title, content)
|
792
|
+
VALUES ('{title}', '{content}')
|
793
|
+
"""
|
794
|
+
)
|
795
|
+
|
796
|
+
async with client.transaction() as tx:
|
797
|
+
# Get connection
|
798
|
+
conn = tx.get_connection()
|
799
|
+
assert conn is not None
|
800
|
+
|
801
|
+
# Test basic query execution
|
802
|
+
from sqlalchemy import text
|
803
|
+
|
804
|
+
result = await conn.execute(text("SELECT COUNT(*) FROM async_get_conn_docs"))
|
805
|
+
rows = result.fetchall()
|
806
|
+
assert rows[0][0] == 3
|
807
|
+
|
808
|
+
# Test parameterized query
|
809
|
+
result = await conn.execute(
|
810
|
+
text("SELECT * FROM async_get_conn_docs WHERE title LIKE :pattern"),
|
811
|
+
{"pattern": "%Test 1%"},
|
812
|
+
)
|
813
|
+
rows = result.fetchall()
|
814
|
+
assert len(rows) == 1
|
815
|
+
assert "Test 1" in rows[0][1] # title column
|
816
|
+
|
817
|
+
finally:
|
818
|
+
# Cleanup
|
819
|
+
try:
|
820
|
+
await client.execute("DROP TABLE async_get_conn_docs")
|
821
|
+
await client.execute(f"DROP DATABASE {test_db}")
|
822
|
+
except Exception as e:
|
823
|
+
print(f"Cleanup warning: {e}")
|
824
|
+
finally:
|
825
|
+
await client.disconnect()
|
826
|
+
|
827
|
+
def test_sync_get_connection_with_other_managers(self, sync_client_setup):
|
828
|
+
"""Test that get_connection works alongside other transaction managers"""
|
829
|
+
client, test_db = sync_client_setup
|
830
|
+
|
831
|
+
with client.transaction() as tx:
|
832
|
+
# Test that we can use both get_connection and other managers
|
833
|
+
conn = tx.get_connection()
|
834
|
+
|
835
|
+
# Use connection directly
|
836
|
+
from sqlalchemy import text
|
837
|
+
|
838
|
+
result = conn.execute(text(f"SELECT COUNT(*) FROM {test_db}.get_conn_docs"))
|
839
|
+
direct_count = result.scalar()
|
840
|
+
|
841
|
+
# Use transaction wrapper execute
|
842
|
+
result = tx.execute(f"SELECT COUNT(*) FROM {test_db}.get_conn_docs")
|
843
|
+
wrapper_count = result.rows[0][0]
|
844
|
+
|
845
|
+
# Both should return the same result
|
846
|
+
assert direct_count == wrapper_count
|
847
|
+
|
848
|
+
# Test that both are in the same transaction
|
849
|
+
conn.execute(
|
850
|
+
text(
|
851
|
+
f"INSERT INTO {test_db}.get_conn_docs (title, content) VALUES ('Direct Insert', 'Using connection directly')"
|
852
|
+
)
|
853
|
+
)
|
854
|
+
tx.execute(
|
855
|
+
f"INSERT INTO {test_db}.get_conn_docs (title, content) VALUES ('Wrapper Insert', 'Using transaction wrapper')"
|
856
|
+
)
|
857
|
+
|
858
|
+
# Both inserts should be visible within the transaction
|
859
|
+
result = conn.execute(text(f"SELECT COUNT(*) FROM {test_db}.get_conn_docs"))
|
860
|
+
final_count = result.scalar()
|
861
|
+
assert final_count == direct_count + 2
|
862
|
+
|
863
|
+
|
864
|
+
class TestAsyncTransactionManagerConsistency:
|
865
|
+
"""Test that async transaction managers behave consistently with sync versions"""
|
866
|
+
|
867
|
+
@pytest.fixture(scope="function")
|
868
|
+
def sync_client_setup(self):
|
869
|
+
"""Setup sync client for testing"""
|
870
|
+
client = Client()
|
871
|
+
host, port, user, password, database = online_config.get_connection_params()
|
872
|
+
client.connect(host, port, user, password, database)
|
873
|
+
|
874
|
+
# Create test database and table
|
875
|
+
test_db = "sync_async_consistency_test"
|
876
|
+
client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
877
|
+
client.execute(f"USE {test_db}")
|
878
|
+
|
879
|
+
client.execute("DROP TABLE IF EXISTS consistency_docs")
|
880
|
+
client.execute(
|
881
|
+
"""
|
882
|
+
CREATE TABLE consistency_docs (
|
883
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
884
|
+
title VARCHAR(255) NOT NULL,
|
885
|
+
content TEXT NOT NULL,
|
886
|
+
category VARCHAR(100) NOT NULL,
|
887
|
+
priority INT DEFAULT 1
|
888
|
+
)
|
889
|
+
"""
|
890
|
+
)
|
891
|
+
|
892
|
+
# Insert test data
|
893
|
+
test_docs = [
|
894
|
+
("Sync Test 1", "Content for sync test 1", "Category1", 1),
|
895
|
+
("Sync Test 2", "Content for sync test 2", "Category2", 2),
|
896
|
+
("Sync Test 3", "Content for sync test 3", "Category1", 3),
|
897
|
+
]
|
898
|
+
|
899
|
+
for title, content, category, priority in test_docs:
|
900
|
+
client.execute(
|
901
|
+
f"""
|
902
|
+
INSERT INTO consistency_docs (title, content, category, priority)
|
903
|
+
VALUES ('{title}', '{content}', '{category}', {priority})
|
904
|
+
"""
|
905
|
+
)
|
906
|
+
|
907
|
+
yield client, test_db
|
908
|
+
|
909
|
+
# Cleanup
|
910
|
+
try:
|
911
|
+
client.execute("DROP TABLE consistency_docs")
|
912
|
+
client.execute(f"DROP DATABASE {test_db}")
|
913
|
+
except Exception as e:
|
914
|
+
print(f"Cleanup warning: {e}")
|
915
|
+
finally:
|
916
|
+
client.disconnect()
|
917
|
+
|
918
|
+
@pytest_asyncio.fixture(scope="function")
|
919
|
+
async def async_client_setup(self):
|
920
|
+
"""Setup async client for testing"""
|
921
|
+
client = AsyncClient()
|
922
|
+
host, port, user, password, database = online_config.get_connection_params()
|
923
|
+
await client.connect(host, port, user, password, database)
|
924
|
+
|
925
|
+
# Create test database and table
|
926
|
+
test_db = "async_consistency_test"
|
927
|
+
await client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
928
|
+
await client.execute(f"USE {test_db}")
|
929
|
+
|
930
|
+
await client.execute("DROP TABLE IF EXISTS async_consistency_docs")
|
931
|
+
await client.execute(
|
932
|
+
"""
|
933
|
+
CREATE TABLE async_consistency_docs (
|
934
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
935
|
+
title VARCHAR(255) NOT NULL,
|
936
|
+
content TEXT NOT NULL,
|
937
|
+
category VARCHAR(100) NOT NULL,
|
938
|
+
priority INT DEFAULT 1
|
939
|
+
)
|
940
|
+
"""
|
941
|
+
)
|
942
|
+
|
943
|
+
# Insert test data
|
944
|
+
test_docs = [
|
945
|
+
("Async Test 1", "Content for async test 1", "Category1", 1),
|
946
|
+
("Async Test 2", "Content for async test 2", "Category2", 2),
|
947
|
+
("Async Test 3", "Content for async test 3", "Category1", 3),
|
948
|
+
]
|
949
|
+
|
950
|
+
for title, content, category, priority in test_docs:
|
951
|
+
await client.execute(
|
952
|
+
f"""
|
953
|
+
INSERT INTO async_consistency_docs (title, content, category, priority)
|
954
|
+
VALUES ('{title}', '{content}', '{category}', {priority})
|
955
|
+
"""
|
956
|
+
)
|
957
|
+
|
958
|
+
yield client, test_db
|
959
|
+
|
960
|
+
# Cleanup
|
961
|
+
try:
|
962
|
+
await client.execute("DROP TABLE async_consistency_docs")
|
963
|
+
await client.execute(f"DROP DATABASE {test_db}")
|
964
|
+
except Exception as e:
|
965
|
+
print(f"Cleanup warning: {e}")
|
966
|
+
finally:
|
967
|
+
await client.disconnect()
|
968
|
+
|
969
|
+
def test_sync_transaction_managers_availability(self, sync_client_setup):
|
970
|
+
"""Test that all expected transaction managers are available in sync client"""
|
971
|
+
client, test_db = sync_client_setup
|
972
|
+
|
973
|
+
with client.transaction() as tx:
|
974
|
+
# Test that all expected managers are available
|
975
|
+
assert hasattr(tx, 'snapshots')
|
976
|
+
assert hasattr(tx, 'clone')
|
977
|
+
assert hasattr(tx, 'restore')
|
978
|
+
assert hasattr(tx, 'pitr')
|
979
|
+
assert hasattr(tx, 'pubsub')
|
980
|
+
assert hasattr(tx, 'account')
|
981
|
+
assert hasattr(tx, 'vector_ops')
|
982
|
+
assert hasattr(tx, 'fulltext_index')
|
983
|
+
assert hasattr(tx, 'get_connection')
|
984
|
+
assert hasattr(tx, 'get_sqlalchemy_session')
|
985
|
+
|
986
|
+
# Test that managers are the correct types
|
987
|
+
from matrixone.client import (
|
988
|
+
TransactionSnapshotManager,
|
989
|
+
TransactionCloneManager,
|
990
|
+
TransactionRestoreManager,
|
991
|
+
TransactionPitrManager,
|
992
|
+
TransactionPubSubManager,
|
993
|
+
TransactionAccountManager,
|
994
|
+
TransactionVectorIndexManager,
|
995
|
+
TransactionFulltextIndexManager,
|
996
|
+
)
|
997
|
+
|
998
|
+
assert isinstance(tx.snapshots, TransactionSnapshotManager)
|
999
|
+
assert isinstance(tx.clone, TransactionCloneManager)
|
1000
|
+
assert isinstance(tx.restore, TransactionRestoreManager)
|
1001
|
+
assert isinstance(tx.pitr, TransactionPitrManager)
|
1002
|
+
assert isinstance(tx.pubsub, TransactionPubSubManager)
|
1003
|
+
assert isinstance(tx.account, TransactionAccountManager)
|
1004
|
+
assert isinstance(tx.vector_ops, TransactionVectorIndexManager)
|
1005
|
+
assert isinstance(tx.fulltext_index, TransactionFulltextIndexManager)
|
1006
|
+
|
1007
|
+
@pytest.mark.asyncio
|
1008
|
+
async def test_async_transaction_managers_availability(self, async_client_setup):
|
1009
|
+
"""Test that all expected transaction managers are available in async client"""
|
1010
|
+
client, test_db = async_client_setup
|
1011
|
+
|
1012
|
+
async with client.transaction() as tx:
|
1013
|
+
# Test that all expected managers are available
|
1014
|
+
assert hasattr(tx, 'snapshots')
|
1015
|
+
assert hasattr(tx, 'clone')
|
1016
|
+
assert hasattr(tx, 'restore')
|
1017
|
+
assert hasattr(tx, 'pitr')
|
1018
|
+
assert hasattr(tx, 'pubsub')
|
1019
|
+
assert hasattr(tx, 'account')
|
1020
|
+
assert hasattr(tx, 'vector_ops')
|
1021
|
+
assert hasattr(tx, 'fulltext_index')
|
1022
|
+
assert hasattr(tx, 'get_connection')
|
1023
|
+
assert hasattr(tx, 'get_sqlalchemy_session')
|
1024
|
+
|
1025
|
+
# Test that managers are the correct types
|
1026
|
+
from matrixone.async_client import (
|
1027
|
+
AsyncTransactionSnapshotManager,
|
1028
|
+
AsyncTransactionCloneManager,
|
1029
|
+
AsyncTransactionRestoreManager,
|
1030
|
+
AsyncTransactionPitrManager,
|
1031
|
+
AsyncTransactionPubSubManager,
|
1032
|
+
AsyncTransactionAccountManager,
|
1033
|
+
AsyncTransactionVectorIndexManager,
|
1034
|
+
AsyncTransactionFulltextIndexManager,
|
1035
|
+
)
|
1036
|
+
|
1037
|
+
assert isinstance(tx.snapshots, AsyncTransactionSnapshotManager)
|
1038
|
+
assert isinstance(tx.clone, AsyncTransactionCloneManager)
|
1039
|
+
assert isinstance(tx.restore, AsyncTransactionRestoreManager)
|
1040
|
+
assert isinstance(tx.pitr, AsyncTransactionPitrManager)
|
1041
|
+
assert isinstance(tx.pubsub, AsyncTransactionPubSubManager)
|
1042
|
+
assert isinstance(tx.account, AsyncTransactionAccountManager)
|
1043
|
+
assert isinstance(tx.vector_ops, AsyncTransactionVectorIndexManager)
|
1044
|
+
assert isinstance(tx.fulltext_index, AsyncTransactionFulltextIndexManager)
|
1045
|
+
|
1046
|
+
def test_sync_vector_ops_transaction_behavior(self, sync_client_setup):
|
1047
|
+
"""Test sync vector_ops manager behavior in transaction"""
|
1048
|
+
client, test_db = sync_client_setup
|
1049
|
+
|
1050
|
+
with client.transaction() as tx:
|
1051
|
+
# Test that vector_ops manager has execute method
|
1052
|
+
assert hasattr(tx.vector_ops, 'execute')
|
1053
|
+
|
1054
|
+
# Test basic query execution through vector_ops
|
1055
|
+
result = tx.vector_ops.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1056
|
+
assert result.rows[0][0] == 3
|
1057
|
+
|
1058
|
+
# Test that vector_ops uses the same transaction
|
1059
|
+
tx.vector_ops.execute(
|
1060
|
+
f"INSERT INTO {test_db}.consistency_docs (title, content, category, priority) VALUES ('Vector Ops Test', 'Content from vector ops', 'Vector', 1)"
|
1061
|
+
)
|
1062
|
+
|
1063
|
+
# Verify the insert is visible within the transaction
|
1064
|
+
result = tx.vector_ops.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1065
|
+
assert result.rows[0][0] == 4
|
1066
|
+
|
1067
|
+
@pytest.mark.asyncio
|
1068
|
+
async def test_async_vector_ops_transaction_behavior(self, async_client_setup):
|
1069
|
+
"""Test async vector_ops manager behavior in transaction"""
|
1070
|
+
client, test_db = async_client_setup
|
1071
|
+
|
1072
|
+
async with client.transaction() as tx:
|
1073
|
+
# Test that vector_ops manager has execute method
|
1074
|
+
assert hasattr(tx.vector_ops, 'execute')
|
1075
|
+
|
1076
|
+
# Test basic query execution through vector_ops
|
1077
|
+
result = await tx.vector_ops.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1078
|
+
assert result.rows[0][0] == 3
|
1079
|
+
|
1080
|
+
# Test that vector_ops uses the same transaction
|
1081
|
+
await tx.vector_ops.execute(
|
1082
|
+
f"INSERT INTO {test_db}.async_consistency_docs (title, content, category, priority) VALUES ('Async Vector Ops Test', 'Content from async vector ops', 'Vector', 1)"
|
1083
|
+
)
|
1084
|
+
|
1085
|
+
# Verify the insert is visible within the transaction
|
1086
|
+
result = await tx.vector_ops.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1087
|
+
assert result.rows[0][0] == 4
|
1088
|
+
|
1089
|
+
def test_sync_transaction_isolation_consistency(self, sync_client_setup):
|
1090
|
+
"""Test that sync transaction managers maintain proper isolation"""
|
1091
|
+
client, test_db = sync_client_setup
|
1092
|
+
|
1093
|
+
# Get initial count
|
1094
|
+
result = client.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1095
|
+
initial_count = result.rows[0][0]
|
1096
|
+
|
1097
|
+
with client.transaction() as tx:
|
1098
|
+
# Use different managers to perform operations
|
1099
|
+
tx.vector_ops.execute(
|
1100
|
+
f"INSERT INTO {test_db}.consistency_docs (title, content, category, priority) VALUES ('Vector Ops Insert', 'Content', 'Test', 1)"
|
1101
|
+
)
|
1102
|
+
tx.execute(
|
1103
|
+
f"INSERT INTO {test_db}.consistency_docs (title, content, category, priority) VALUES ('Direct Insert', 'Content', 'Test', 1)"
|
1104
|
+
)
|
1105
|
+
|
1106
|
+
# All managers should see the same data within transaction
|
1107
|
+
result1 = tx.vector_ops.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1108
|
+
result2 = tx.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1109
|
+
|
1110
|
+
expected_count = initial_count + 2
|
1111
|
+
assert result1.rows[0][0] == expected_count
|
1112
|
+
assert result2.rows[0][0] == expected_count
|
1113
|
+
|
1114
|
+
# Outside transaction should not see the changes yet
|
1115
|
+
result_outside = client.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1116
|
+
assert result_outside.rows[0][0] == initial_count
|
1117
|
+
|
1118
|
+
# After transaction commit, all changes should be visible
|
1119
|
+
result_final = client.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1120
|
+
assert result_final.rows[0][0] == initial_count + 2
|
1121
|
+
|
1122
|
+
@pytest.mark.asyncio
|
1123
|
+
async def test_async_transaction_isolation_consistency(self, async_client_setup):
|
1124
|
+
"""Test that async transaction managers maintain proper isolation"""
|
1125
|
+
client, test_db = async_client_setup
|
1126
|
+
|
1127
|
+
# Get initial count
|
1128
|
+
result = await client.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1129
|
+
initial_count = result.rows[0][0]
|
1130
|
+
|
1131
|
+
async with client.transaction() as tx:
|
1132
|
+
# Use different managers to perform operations
|
1133
|
+
await tx.vector_ops.execute(
|
1134
|
+
f"INSERT INTO {test_db}.async_consistency_docs (title, content, category, priority) VALUES ('Async Vector Ops Insert', 'Content', 'Test', 1)"
|
1135
|
+
)
|
1136
|
+
await tx.execute(
|
1137
|
+
f"INSERT INTO {test_db}.async_consistency_docs (title, content, category, priority) VALUES ('Async Direct Insert', 'Content', 'Test', 1)"
|
1138
|
+
)
|
1139
|
+
|
1140
|
+
# All managers should see the same data within transaction
|
1141
|
+
result1 = await tx.vector_ops.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1142
|
+
result2 = await tx.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1143
|
+
|
1144
|
+
expected_count = initial_count + 2
|
1145
|
+
assert result1.rows[0][0] == expected_count
|
1146
|
+
assert result2.rows[0][0] == expected_count
|
1147
|
+
|
1148
|
+
# Outside transaction should not see the changes yet
|
1149
|
+
result_outside = await client.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1150
|
+
assert result_outside.rows[0][0] == initial_count
|
1151
|
+
|
1152
|
+
# After transaction commit, all changes should be visible
|
1153
|
+
result_final = await client.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1154
|
+
assert result_final.rows[0][0] == initial_count + 2
|
1155
|
+
|
1156
|
+
def test_sync_async_behavior_consistency(self, sync_client_setup):
|
1157
|
+
"""Test that sync and async transaction managers behave consistently"""
|
1158
|
+
client, test_db = sync_client_setup
|
1159
|
+
|
1160
|
+
# Get initial count first
|
1161
|
+
result = client.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1162
|
+
initial_count = result.rows[0][0]
|
1163
|
+
|
1164
|
+
with client.transaction() as tx:
|
1165
|
+
# Test that all managers can execute queries
|
1166
|
+
managers_to_test = [
|
1167
|
+
('vector_ops', tx.vector_ops),
|
1168
|
+
('direct', tx),
|
1169
|
+
]
|
1170
|
+
|
1171
|
+
for i, (manager_name, manager) in enumerate(managers_to_test):
|
1172
|
+
# Check current count (should be initial + number of previous inserts)
|
1173
|
+
expected_count = initial_count + i
|
1174
|
+
result = manager.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1175
|
+
assert (
|
1176
|
+
result.rows[0][0] == expected_count
|
1177
|
+
), f"Manager {manager_name} returned unexpected count {result.rows[0][0]}, expected {expected_count}"
|
1178
|
+
|
1179
|
+
# Test insert through each manager
|
1180
|
+
manager.execute(
|
1181
|
+
f"INSERT INTO {test_db}.consistency_docs (title, content, category, priority) VALUES ('{manager_name} Test', 'Content', 'Test', 1)"
|
1182
|
+
)
|
1183
|
+
|
1184
|
+
# Verify insert is visible
|
1185
|
+
result = manager.execute(f"SELECT COUNT(*) FROM {test_db}.consistency_docs")
|
1186
|
+
assert result.rows[0][0] == expected_count + 1, f"Manager {manager_name} did not see its own insert"
|
1187
|
+
|
1188
|
+
@pytest.mark.asyncio
|
1189
|
+
async def test_async_async_behavior_consistency(self, async_client_setup):
|
1190
|
+
"""Test that async transaction managers behave consistently"""
|
1191
|
+
client, test_db = async_client_setup
|
1192
|
+
|
1193
|
+
# Get initial count first
|
1194
|
+
result = await client.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1195
|
+
initial_count = result.rows[0][0]
|
1196
|
+
|
1197
|
+
async with client.transaction() as tx:
|
1198
|
+
# Test that all managers can execute queries
|
1199
|
+
managers_to_test = [
|
1200
|
+
('vector_ops', tx.vector_ops),
|
1201
|
+
('direct', tx),
|
1202
|
+
]
|
1203
|
+
|
1204
|
+
for i, (manager_name, manager) in enumerate(managers_to_test):
|
1205
|
+
# Check current count (should be initial + number of previous inserts)
|
1206
|
+
expected_count = initial_count + i
|
1207
|
+
result = await manager.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1208
|
+
assert (
|
1209
|
+
result.rows[0][0] == expected_count
|
1210
|
+
), f"Manager {manager_name} returned unexpected count {result.rows[0][0]}, expected {expected_count}"
|
1211
|
+
|
1212
|
+
# Test insert through each manager
|
1213
|
+
await manager.execute(
|
1214
|
+
f"INSERT INTO {test_db}.async_consistency_docs (title, content, category, priority) VALUES ('{manager_name} Test', 'Content', 'Test', 1)"
|
1215
|
+
)
|
1216
|
+
|
1217
|
+
# Verify insert is visible
|
1218
|
+
result = await manager.execute(f"SELECT COUNT(*) FROM {test_db}.async_consistency_docs")
|
1219
|
+
assert result.rows[0][0] == expected_count + 1, f"Manager {manager_name} did not see its own insert"
|