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.
Files changed (122) hide show
  1. matrixone/__init__.py +155 -0
  2. matrixone/account.py +723 -0
  3. matrixone/async_client.py +3913 -0
  4. matrixone/async_metadata_manager.py +311 -0
  5. matrixone/async_orm.py +123 -0
  6. matrixone/async_vector_index_manager.py +633 -0
  7. matrixone/base_client.py +208 -0
  8. matrixone/client.py +4672 -0
  9. matrixone/config.py +452 -0
  10. matrixone/connection_hooks.py +286 -0
  11. matrixone/exceptions.py +89 -0
  12. matrixone/logger.py +782 -0
  13. matrixone/metadata.py +820 -0
  14. matrixone/moctl.py +219 -0
  15. matrixone/orm.py +2277 -0
  16. matrixone/pitr.py +646 -0
  17. matrixone/pubsub.py +771 -0
  18. matrixone/restore.py +411 -0
  19. matrixone/search_vector_index.py +1176 -0
  20. matrixone/snapshot.py +550 -0
  21. matrixone/sql_builder.py +844 -0
  22. matrixone/sqlalchemy_ext/__init__.py +161 -0
  23. matrixone/sqlalchemy_ext/adapters.py +163 -0
  24. matrixone/sqlalchemy_ext/dialect.py +534 -0
  25. matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
  26. matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
  27. matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
  28. matrixone/sqlalchemy_ext/ivf_config.py +252 -0
  29. matrixone/sqlalchemy_ext/table_builder.py +351 -0
  30. matrixone/sqlalchemy_ext/vector_index.py +1721 -0
  31. matrixone/sqlalchemy_ext/vector_type.py +948 -0
  32. matrixone/version.py +580 -0
  33. matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
  34. matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
  35. matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
  36. matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
  37. matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
  38. matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
  39. tests/__init__.py +19 -0
  40. tests/offline/__init__.py +20 -0
  41. tests/offline/conftest.py +77 -0
  42. tests/offline/test_account.py +703 -0
  43. tests/offline/test_async_client_query_comprehensive.py +1218 -0
  44. tests/offline/test_basic.py +54 -0
  45. tests/offline/test_case_sensitivity.py +227 -0
  46. tests/offline/test_connection_hooks_offline.py +287 -0
  47. tests/offline/test_dialect_schema_handling.py +609 -0
  48. tests/offline/test_explain_methods.py +346 -0
  49. tests/offline/test_filter_logical_in.py +237 -0
  50. tests/offline/test_fulltext_search_comprehensive.py +795 -0
  51. tests/offline/test_ivf_config.py +249 -0
  52. tests/offline/test_join_methods.py +281 -0
  53. tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
  54. tests/offline/test_logical_in_method.py +237 -0
  55. tests/offline/test_matrixone_version_parsing.py +264 -0
  56. tests/offline/test_metadata_offline.py +557 -0
  57. tests/offline/test_moctl.py +300 -0
  58. tests/offline/test_moctl_simple.py +251 -0
  59. tests/offline/test_model_support_offline.py +359 -0
  60. tests/offline/test_model_support_simple.py +225 -0
  61. tests/offline/test_pinecone_filter_offline.py +377 -0
  62. tests/offline/test_pitr.py +585 -0
  63. tests/offline/test_pubsub.py +712 -0
  64. tests/offline/test_query_update.py +283 -0
  65. tests/offline/test_restore.py +445 -0
  66. tests/offline/test_snapshot_comprehensive.py +384 -0
  67. tests/offline/test_sql_escaping_edge_cases.py +551 -0
  68. tests/offline/test_sqlalchemy_integration.py +382 -0
  69. tests/offline/test_sqlalchemy_vector_integration.py +434 -0
  70. tests/offline/test_table_builder.py +198 -0
  71. tests/offline/test_unified_filter.py +398 -0
  72. tests/offline/test_unified_transaction.py +495 -0
  73. tests/offline/test_vector_index.py +238 -0
  74. tests/offline/test_vector_operations.py +688 -0
  75. tests/offline/test_vector_type.py +174 -0
  76. tests/offline/test_version_core.py +328 -0
  77. tests/offline/test_version_management.py +372 -0
  78. tests/offline/test_version_standalone.py +652 -0
  79. tests/online/__init__.py +20 -0
  80. tests/online/conftest.py +216 -0
  81. tests/online/test_account_management.py +194 -0
  82. tests/online/test_advanced_features.py +344 -0
  83. tests/online/test_async_client_interfaces.py +330 -0
  84. tests/online/test_async_client_online.py +285 -0
  85. tests/online/test_async_model_insert_online.py +293 -0
  86. tests/online/test_async_orm_online.py +300 -0
  87. tests/online/test_async_simple_query_online.py +802 -0
  88. tests/online/test_async_transaction_simple_query.py +300 -0
  89. tests/online/test_basic_connection.py +130 -0
  90. tests/online/test_client_online.py +238 -0
  91. tests/online/test_config.py +90 -0
  92. tests/online/test_config_validation.py +123 -0
  93. tests/online/test_connection_hooks_new_online.py +217 -0
  94. tests/online/test_dialect_schema_handling_online.py +331 -0
  95. tests/online/test_filter_logical_in_online.py +374 -0
  96. tests/online/test_fulltext_comprehensive.py +1773 -0
  97. tests/online/test_fulltext_label_online.py +433 -0
  98. tests/online/test_fulltext_search_online.py +842 -0
  99. tests/online/test_ivf_stats_online.py +506 -0
  100. tests/online/test_logger_integration.py +311 -0
  101. tests/online/test_matrixone_query_orm.py +540 -0
  102. tests/online/test_metadata_online.py +579 -0
  103. tests/online/test_model_insert_online.py +255 -0
  104. tests/online/test_mysql_driver_validation.py +213 -0
  105. tests/online/test_orm_advanced_features.py +2022 -0
  106. tests/online/test_orm_cte_integration.py +269 -0
  107. tests/online/test_orm_online.py +270 -0
  108. tests/online/test_pinecone_filter.py +708 -0
  109. tests/online/test_pubsub_operations.py +352 -0
  110. tests/online/test_query_methods.py +225 -0
  111. tests/online/test_query_update_online.py +433 -0
  112. tests/online/test_search_vector_index.py +557 -0
  113. tests/online/test_simple_fulltext_online.py +915 -0
  114. tests/online/test_snapshot_comprehensive.py +998 -0
  115. tests/online/test_sqlalchemy_engine_integration.py +336 -0
  116. tests/online/test_sqlalchemy_integration.py +425 -0
  117. tests/online/test_transaction_contexts.py +1219 -0
  118. tests/online/test_transaction_insert_methods.py +356 -0
  119. tests/online/test_transaction_query_methods.py +288 -0
  120. tests/online/test_unified_filter_online.py +529 -0
  121. tests/online/test_vector_comprehensive.py +706 -0
  122. tests/online/test_version_management.py +291 -0
@@ -0,0 +1,802 @@
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
+
17
+ Online tests for AsyncClient SimpleFulltextQueryBuilder functionality.
18
+
19
+ Tests async simple_query methods against a real MatrixOne database to verify
20
+ that generated SQL works correctly and returns expected results.
21
+ """
22
+
23
+ import pytest
24
+ import pytest_asyncio
25
+ from sqlalchemy import Column, Integer, String, Text
26
+ from sqlalchemy.orm import declarative_base
27
+
28
+ from matrixone.async_client import AsyncClient
29
+ from matrixone.config import get_connection_kwargs
30
+ from matrixone.exceptions import QueryError
31
+ from matrixone.sqlalchemy_ext import boolean_match, natural_match
32
+
33
+ # SQLAlchemy model for testing
34
+ Base = declarative_base()
35
+
36
+
37
+ class AsyncArticle(Base):
38
+ __tablename__ = "test_async_simple_query_articles"
39
+
40
+ id = Column(Integer, primary_key=True, autoincrement=True)
41
+ title = Column(String(255), nullable=False)
42
+ content = Column(Text, nullable=False)
43
+ category = Column(String(100), nullable=False)
44
+ tags = Column(String(500))
45
+ author = Column(String(100))
46
+ published_date = Column(String(20))
47
+
48
+
49
+ class TestAsyncSimpleQueryOnline:
50
+ """Online tests for AsyncSimpleFulltextQueryBuilder with real database."""
51
+
52
+ @pytest_asyncio.fixture(scope="function")
53
+ async def async_client_setup(self):
54
+ """Set up test database and data."""
55
+ # Get connection parameters
56
+ conn_params = get_connection_kwargs()
57
+ client_params = {k: v for k, v in conn_params.items() if k in ['host', 'port', 'user', 'password', 'database']}
58
+
59
+ client = AsyncClient()
60
+ await client.connect(**client_params)
61
+
62
+ test_db = "test_async_simple_query"
63
+
64
+ # Enable experimental fulltext index
65
+ try:
66
+ await client.execute("SET experimental_fulltext_index=1")
67
+ except Exception as e:
68
+ print(f"Fulltext index setup warning: {e}")
69
+
70
+ test_db = "test_async_simple_query"
71
+ try:
72
+ await client.execute(f"DROP DATABASE IF EXISTS {test_db}")
73
+ await client.execute(f"CREATE DATABASE {test_db}")
74
+ await client.execute(f"USE {test_db}")
75
+ except Exception as e:
76
+ print(f"Database setup warning: {e}")
77
+
78
+ # Create test table
79
+ await client.execute("DROP TABLE IF EXISTS test_async_simple_query_articles")
80
+ await client.execute(
81
+ """
82
+ CREATE TABLE IF NOT EXISTS test_async_simple_query_articles (
83
+ id INT AUTO_INCREMENT PRIMARY KEY,
84
+ title VARCHAR(255) NOT NULL,
85
+ content TEXT NOT NULL,
86
+ category VARCHAR(100) NOT NULL,
87
+ tags VARCHAR(500),
88
+ author VARCHAR(100),
89
+ published_date VARCHAR(20)
90
+ )
91
+ """
92
+ )
93
+
94
+ # Insert test data
95
+ test_articles = [
96
+ (
97
+ 1,
98
+ "Python Async Programming",
99
+ "Learn Python async programming with asyncio and async/await",
100
+ "Programming",
101
+ "python,async,asyncio,programming",
102
+ "Alice",
103
+ "2024-01-15",
104
+ ),
105
+ (
106
+ 2,
107
+ "Machine Learning with Python",
108
+ "Complete guide to machine learning algorithms and implementation",
109
+ "AI",
110
+ "python,ml,ai,algorithms",
111
+ "Bob",
112
+ "2024-01-20",
113
+ ),
114
+ (
115
+ 3,
116
+ "JavaScript Async Development",
117
+ "Modern JavaScript development with async/await and promises",
118
+ "Programming",
119
+ "javascript,async,promises,web",
120
+ "Charlie",
121
+ "2024-01-25",
122
+ ),
123
+ (
124
+ 4,
125
+ "Data Science Fundamentals",
126
+ "Introduction to data science concepts and tools",
127
+ "AI",
128
+ "data,science,analytics,python",
129
+ "David",
130
+ "2024-02-01",
131
+ ),
132
+ (
133
+ 5,
134
+ "Deprecated Python Libraries",
135
+ "Old Python libraries that should be avoided",
136
+ "Programming",
137
+ "python,deprecated,legacy",
138
+ "Eve",
139
+ "2024-02-05",
140
+ ),
141
+ (
142
+ 6,
143
+ "Advanced Machine Learning",
144
+ "Deep learning and neural networks with TensorFlow",
145
+ "AI",
146
+ "ai,deeplearning,tensorflow,python",
147
+ "Frank",
148
+ "2024-02-10",
149
+ ),
150
+ (
151
+ 7,
152
+ "Web Development Best Practices",
153
+ "Modern practices for building web applications",
154
+ "Programming",
155
+ "web,best practices,modern,async",
156
+ "Grace",
157
+ "2024-02-15",
158
+ ),
159
+ (
160
+ 8,
161
+ "Artificial Intelligence Overview",
162
+ "Introduction to AI concepts and applications",
163
+ "AI",
164
+ "ai,overview,introduction",
165
+ "Henry",
166
+ "2024-02-20",
167
+ ),
168
+ (
169
+ 9,
170
+ "Async Database Operations",
171
+ "How to perform async database operations efficiently",
172
+ "Database",
173
+ "async,database,operations,performance",
174
+ "Ivy",
175
+ "2024-02-25",
176
+ ),
177
+ (
178
+ 10,
179
+ "Python Web Frameworks",
180
+ "Comparison of Python web frameworks including async support",
181
+ "Programming",
182
+ "python,web,frameworks,async,fastapi",
183
+ "Jack",
184
+ "2024-03-01",
185
+ ),
186
+ ]
187
+
188
+ for article in test_articles:
189
+ title_escaped = article[1].replace("'", "''")
190
+ content_escaped = article[2].replace("'", "''")
191
+ category_escaped = article[3].replace("'", "''")
192
+ tags_escaped = article[4].replace("'", "''") if article[4] else ''
193
+ author_escaped = article[5].replace("'", "''")
194
+ date_escaped = article[6].replace("'", "''")
195
+
196
+ await client.execute(
197
+ f"""
198
+ INSERT INTO test_async_simple_query_articles (id, title, content, category, tags, author, published_date)
199
+ VALUES ({article[0]}, '{title_escaped}', '{content_escaped}', '{category_escaped}', '{tags_escaped}', '{author_escaped}', '{date_escaped}')
200
+ """
201
+ )
202
+
203
+ # Create fulltext index - use only the columns that simple_query will search
204
+ try:
205
+ await client.fulltext_index.create("test_async_simple_query_articles", "ft_async_articles", ["title", "content"])
206
+ except Exception as e:
207
+ print(f"Fulltext index creation warning: {e}")
208
+
209
+ yield client, test_db
210
+
211
+ # Cleanup
212
+ try:
213
+ await client.execute(f"DROP DATABASE IF EXISTS {test_db}")
214
+ await client.disconnect()
215
+ except Exception as e:
216
+ print(f"Cleanup warning: {e}")
217
+
218
+ @pytest.mark.asyncio
219
+ async def test_async_basic_natural_language_search(self, async_client_setup):
220
+ """Test basic natural language search with async client."""
221
+ client, test_db = async_client_setup
222
+
223
+ results = await client.query(AsyncArticle).filter(boolean_match("title", "content").encourage("python")).execute()
224
+
225
+ rows = results.fetchall()
226
+ assert isinstance(rows, list)
227
+ assert len(rows) > 0
228
+
229
+ # Should find Python-related articles
230
+ found_python = any("Python" in row[1] for row in rows) # title column
231
+ assert found_python, "Should find articles containing 'Python'"
232
+
233
+ @pytest.mark.asyncio
234
+ async def test_async_boolean_mode_required_terms(self, async_client_setup):
235
+ """Test boolean mode with required terms using async client."""
236
+ client, test_db = async_client_setup
237
+ results = await (
238
+ client.query(AsyncArticle).filter(boolean_match("title", "content").must("python").must("async")).execute()
239
+ )
240
+
241
+ rows = results.fetchall()
242
+ assert isinstance(rows, list)
243
+ assert len(rows) > 0
244
+
245
+ # Should find articles that have both "python" AND "async"
246
+ if rows:
247
+ for row in rows:
248
+ title_content = f"{row[1]} {row[2]}".lower() # title + content
249
+ assert "python" in title_content and "async" in title_content
250
+
251
+ @pytest.mark.asyncio
252
+ async def test_async_boolean_mode_excluded_terms(self, async_client_setup):
253
+ """Test boolean mode with excluded terms using async client."""
254
+ client, test_db = async_client_setup
255
+ results = await (
256
+ client.query(AsyncArticle)
257
+ .filter(boolean_match("title", "content").must("python").must_not("deprecated"))
258
+ .execute()
259
+ )
260
+
261
+ rows = results.fetchall()
262
+ assert isinstance(rows, list)
263
+ assert len(rows) > 0
264
+
265
+ # Should find articles but exclude deprecated ones
266
+ for row in rows:
267
+ title_content = f"{row[1]} {row[2]}".lower() # title + content
268
+ assert "deprecated" not in title_content
269
+
270
+ @pytest.mark.asyncio
271
+ async def test_async_search_with_score(self, async_client_setup):
272
+ """Test search with relevance scoring using async client."""
273
+ client, test_db = async_client_setup
274
+ results = await client.query(
275
+ AsyncArticle.id,
276
+ AsyncArticle.title,
277
+ AsyncArticle.content,
278
+ boolean_match("title", "content").encourage("machine", "learning").label("score"),
279
+ ).execute()
280
+
281
+ rows = results.fetchall()
282
+ assert isinstance(rows, list)
283
+ assert len(rows) > 0
284
+
285
+ # Check that score column is included
286
+ assert len(results.columns) == 4 # id, title, content, score
287
+ assert "score" in results.columns
288
+ score_column_index = results.columns.index("score")
289
+
290
+ # Verify score values are numeric
291
+ for row in rows:
292
+ score = row[score_column_index]
293
+ assert isinstance(score, (int, float)), f"Score should be numeric, got {type(score)}"
294
+ assert score >= 0, f"Score should be non-negative, got {score}"
295
+
296
+ @pytest.mark.asyncio
297
+ async def test_async_search_with_custom_score_alias(self, async_client_setup):
298
+ """Test search with custom score alias using async client."""
299
+ client, test_db = async_client_setup
300
+
301
+ results = await client.query(
302
+ AsyncArticle.id,
303
+ AsyncArticle.title,
304
+ AsyncArticle.content,
305
+ boolean_match("title", "content").encourage("artificial", "intelligence").label("relevance"),
306
+ ).execute()
307
+
308
+ assert isinstance(results.fetchall(), list)
309
+ # Score column should be present (custom alias doesn't change result structure)
310
+ if results.fetchall():
311
+ score_index = len(results.columns) - 1
312
+ for row in results.fetchall():
313
+ score = row[score_index]
314
+ assert isinstance(score, (int, float))
315
+
316
+ @pytest.mark.asyncio
317
+ async def test_async_search_with_where_conditions(self, async_client_setup):
318
+ """Test search with additional WHERE conditions using async client."""
319
+ client, test_db = async_client_setup
320
+ results = await (
321
+ client.query(AsyncArticle)
322
+ .filter(boolean_match("title", "content").encourage("programming"), AsyncArticle.category == "Programming")
323
+ .execute()
324
+ )
325
+
326
+ rows = results.fetchall()
327
+ assert isinstance(rows, list)
328
+ assert len(rows) > 0
329
+
330
+ assert isinstance(results.fetchall(), list)
331
+ # All results should be in Programming category
332
+ for row in results.fetchall():
333
+ category = row[3] # category column
334
+ assert category == "Programming"
335
+
336
+ @pytest.mark.asyncio
337
+ async def test_async_search_with_ordering_and_limit(self, async_client_setup):
338
+ """Test search with ordering and pagination using async client."""
339
+ client, test_db = async_client_setup
340
+
341
+ results = await (
342
+ client.query(
343
+ AsyncArticle.id,
344
+ AsyncArticle.title,
345
+ AsyncArticle.content,
346
+ boolean_match("title", "content").encourage("development").label("score"),
347
+ )
348
+ .limit(3)
349
+ .execute()
350
+ )
351
+
352
+ rows = results.fetchall()
353
+ assert isinstance(rows, list)
354
+ assert len(rows) <= 3 # Should respect limit
355
+
356
+ if len(rows) > 1:
357
+ # Verify results are ordered by score (descending)
358
+ score_index = len(rows) - 1
359
+ scores = [row[score_index] for row in rows]
360
+ assert scores == sorted(scores, reverse=True), "Results should be ordered by score DESC"
361
+
362
+ @pytest.mark.asyncio
363
+ async def test_async_search_by_category(self, async_client_setup):
364
+ """Test filtering by category using async client."""
365
+ client, test_db = async_client_setup
366
+ # Search AI category
367
+ ai_results = await (
368
+ client.query(AsyncArticle)
369
+ .filter(natural_match("title", "content", query="learning"), AsyncArticle.category == "AI")
370
+ .execute()
371
+ )
372
+
373
+ # Search Programming category
374
+ prog_results = await (
375
+ client.query(AsyncArticle)
376
+ .filter(boolean_match("title", "content").encourage("programming"), AsyncArticle.category == "Programming")
377
+ .execute()
378
+ )
379
+
380
+ ai_rows = ai_results.fetchall()
381
+ prog_rows = prog_results.fetchall()
382
+
383
+ # Verify results are from correct categories
384
+ for row in ai_rows:
385
+ assert row[3] == "AI"
386
+
387
+ for row in prog_rows:
388
+ assert row[3] == "Programming"
389
+
390
+ @pytest.mark.asyncio
391
+ async def test_async_complex_boolean_search(self, async_client_setup):
392
+ """Test complex boolean search with multiple terms using async client."""
393
+ client, test_db = async_client_setup
394
+
395
+ results = await (
396
+ client.query(AsyncArticle)
397
+ .filter(
398
+ boolean_match("title", "content").must("web").must_not("deprecated", "legacy"),
399
+ AsyncArticle.category == "Programming",
400
+ )
401
+ .execute()
402
+ )
403
+
404
+ rows = results.fetchall()
405
+ assert isinstance(rows, list)
406
+ # Should find articles without deprecated content
407
+ for row in rows:
408
+ title_content = f"{row[1]} {row[2]}".lower()
409
+ assert "deprecated" not in title_content
410
+ assert "legacy" not in title_content
411
+ assert row[3] == "Programming"
412
+
413
+ @pytest.mark.asyncio
414
+ async def test_async_empty_search_results(self, async_client_setup):
415
+ """Test search that should return no results using async client."""
416
+ client, test_db = async_client_setup
417
+ results = await (
418
+ client.query("test_async_simple_query_articles")
419
+ .filter(natural_match("title", "content", query="nonexistent_term_xyz123"))
420
+ .execute()
421
+ )
422
+
423
+ rows = results.fetchall()
424
+
425
+ assert isinstance(rows, list)
426
+ assert len(rows) == 0
427
+
428
+ @pytest.mark.asyncio
429
+ async def test_async_chinese_text_search(self, async_client_setup):
430
+ """Test search with non-English text using async client."""
431
+ # Insert a Chinese article for testing
432
+ client, test_db = async_client_setup
433
+ await client.execute(
434
+ """
435
+ INSERT INTO test_async_simple_query_articles (title, content, category, tags)
436
+ VALUES ('中文异步测试', '这是一个中文异步全文搜索测试', 'Test', 'chinese,async,test')
437
+ """
438
+ )
439
+
440
+ results = await (
441
+ client.query("test_async_simple_query_articles")
442
+ .filter(natural_match("title", "content", query="中文"))
443
+ .execute()
444
+ )
445
+
446
+ assert isinstance(results.fetchall(), list)
447
+ # Should find the Chinese article
448
+ if results.fetchall():
449
+ found_chinese = any("中文" in row[1] for row in results.fetchall())
450
+ assert found_chinese
451
+
452
+ @pytest.mark.asyncio
453
+ async def test_async_error_handling_invalid_table(self, async_client_setup):
454
+ """Test error handling for invalid table using async client."""
455
+ client, test_db = async_client_setup
456
+ with pytest.raises(QueryError):
457
+ await client.query("nonexistent_table").filter(natural_match("title", "content", query="test")).execute()
458
+
459
+ @pytest.mark.asyncio
460
+ async def test_async_basic_search_with_columns(self, async_client_setup):
461
+ """Test basic search with specific columns using async client."""
462
+ client, test_db = async_client_setup
463
+ results = await client.query(
464
+ AsyncArticle.title, AsyncArticle.content, boolean_match("title", "content").encourage("python")
465
+ ).execute()
466
+
467
+ rows = results.fetchall()
468
+ assert isinstance(rows, list)
469
+ assert len(rows) > 0
470
+
471
+ @pytest.mark.asyncio
472
+ async def test_async_search_programming(self, async_client_setup):
473
+ """Test search for programming-related content using async client."""
474
+ client, test_db = async_client_setup
475
+ results = await client.query(
476
+ AsyncArticle.title, AsyncArticle.content, boolean_match("title", "content").encourage("programming")
477
+ ).execute()
478
+
479
+ rows = results.fetchall()
480
+
481
+ assert isinstance(rows, list)
482
+ assert len(rows) > 0
483
+ # Verify we get programming-related results
484
+ for row in rows:
485
+ title_content = f"{row[0]} {row[1]}".lower()
486
+ assert "programming" in title_content
487
+
488
+ @pytest.mark.asyncio
489
+ async def test_async_limit_with_offset(self, async_client_setup):
490
+ """Test limit with offset using async client."""
491
+ client, test_db = async_client_setup
492
+ # Get first 3 results
493
+ first_results = await (
494
+ client.query(AsyncArticle.title, AsyncArticle.content, boolean_match("title", "content").encourage("python"))
495
+ .limit(3)
496
+ .execute()
497
+ )
498
+
499
+ # Get next 3 results (offset 3)
500
+ next_results = await (
501
+ client.query(AsyncArticle.title, AsyncArticle.content, boolean_match("title", "content").encourage("python"))
502
+ .limit(3)
503
+ .offset(3)
504
+ .execute()
505
+ )
506
+
507
+ first_rows = first_results.fetchall()
508
+ next_rows = next_results.fetchall()
509
+
510
+ assert isinstance(first_rows, list)
511
+ assert isinstance(next_rows, list)
512
+
513
+ # Results should be different (if there are enough results)
514
+ if len(first_rows) > 0 and len(next_rows) > 0:
515
+ assert first_rows[0] != next_rows[0], "Offset should return different results"
516
+
517
+ @pytest.mark.asyncio
518
+ async def test_async_multiple_where_conditions(self, async_client_setup):
519
+ """Test multiple WHERE conditions using async client."""
520
+ client, test_db = async_client_setup
521
+ results = await (
522
+ client.query(AsyncArticle)
523
+ .filter(boolean_match("title", "content").encourage("python"), AsyncArticle.category == "Programming")
524
+ .execute()
525
+ )
526
+
527
+ rows = results.fetchall()
528
+ assert isinstance(rows, list)
529
+ # All results should match the category condition
530
+ for row in rows:
531
+ assert row[3] in ["Programming", "AI"] # category - both are valid since we search for "python"
532
+
533
+ @pytest.mark.asyncio
534
+ async def test_async_concurrent_searches(self, async_client_setup):
535
+ """Test concurrent async searches."""
536
+ import asyncio
537
+
538
+ client, test_db = async_client_setup
539
+
540
+ # Create multiple search tasks using database prefix to avoid connection pool issues
541
+ async def search_task(search_term):
542
+ # Use database prefix instead of USE statement to avoid connection pool issues
543
+ return await client.query(
544
+ AsyncArticle.title, AsyncArticle.content, boolean_match("title", "content").search(search_term)
545
+ ).execute()
546
+
547
+ # Execute all searches concurrently
548
+ tasks = [search_task("python"), search_task("async"), search_task("machine learning")]
549
+
550
+ results = await asyncio.gather(*tasks, return_exceptions=True)
551
+
552
+ # Check that all searches completed (either successfully or with expected exceptions)
553
+ assert len(results) == 3
554
+ for i, result in enumerate(results):
555
+ if isinstance(result, Exception):
556
+ print(f"Search {i} failed with: {result}")
557
+ # If it's a table not found error, that's the issue we're investigating
558
+ if "does not exist" in str(result):
559
+ pytest.fail(f"Table not found during concurrent search {i}: {result}")
560
+ else:
561
+ rows = result.fetchall()
562
+ assert isinstance(rows, list)
563
+ assert len(rows) > 0
564
+
565
+
566
+ class TestAsyncTransactionSimpleQueryOnline:
567
+ """Online tests for AsyncTransactionSimpleFulltextQueryBuilder with real database."""
568
+
569
+ @pytest_asyncio.fixture(scope="function")
570
+ async def async_tx_client_setup(self):
571
+ """Set up test database and data."""
572
+ # Get connection parameters
573
+ conn_params = get_connection_kwargs()
574
+ client_params = {k: v for k, v in conn_params.items() if k in ['host', 'port', 'user', 'password', 'database']}
575
+
576
+ client = AsyncClient()
577
+ await client.connect(**client_params)
578
+
579
+ test_db = "test_async_tx_simple_query"
580
+
581
+ # Enable experimental fulltext index
582
+ try:
583
+ await client.execute("SET experimental_fulltext_index=1")
584
+ except Exception as e:
585
+ print(f"Fulltext index setup warning: {e}")
586
+
587
+ test_db = "test_async_tx_simple_query"
588
+ try:
589
+ await client.execute(f"DROP DATABASE IF EXISTS {test_db}")
590
+ await client.execute(f"CREATE DATABASE {test_db}")
591
+ await client.execute(f"USE {test_db}")
592
+ except Exception as e:
593
+ print(f"Database setup warning: {e}")
594
+
595
+ # Create test table
596
+ await client.execute("DROP TABLE IF EXISTS test_async_simple_query_articles")
597
+ await client.execute(
598
+ """
599
+ CREATE TABLE IF NOT EXISTS test_async_simple_query_articles (
600
+ id INT AUTO_INCREMENT PRIMARY KEY,
601
+ title VARCHAR(255) NOT NULL,
602
+ content TEXT NOT NULL,
603
+ category VARCHAR(100) NOT NULL,
604
+ tags VARCHAR(500),
605
+ author VARCHAR(100),
606
+ published_date VARCHAR(20)
607
+ )
608
+ """
609
+ )
610
+
611
+ # Insert test data
612
+ test_articles = [
613
+ (
614
+ 1,
615
+ "Transaction Python Guide",
616
+ "Learn Python database transactions",
617
+ "Programming",
618
+ "python,transaction,database",
619
+ "Alice",
620
+ "2024-01-01",
621
+ ),
622
+ (
623
+ 2,
624
+ "Async Transaction Patterns",
625
+ "Modern async transaction patterns",
626
+ "Programming",
627
+ "async,transaction,patterns",
628
+ "Bob",
629
+ "2024-01-02",
630
+ ),
631
+ (
632
+ 3,
633
+ "Database Transaction Best Practices",
634
+ "Best practices for database transactions",
635
+ "Database",
636
+ "database,transaction,best practices",
637
+ "Charlie",
638
+ "2024-01-03",
639
+ ),
640
+ (
641
+ 4,
642
+ "Python Async Database",
643
+ "Async database operations in Python",
644
+ "Programming",
645
+ "python,async,database",
646
+ "David",
647
+ "2024-01-04",
648
+ ),
649
+ (
650
+ 5,
651
+ "Transaction Isolation Levels",
652
+ "Understanding transaction isolation levels",
653
+ "Database",
654
+ "transaction,isolation,levels",
655
+ "Eve",
656
+ "2024-01-05",
657
+ ),
658
+ ]
659
+
660
+ for article in test_articles:
661
+ title_escaped = article[1].replace("'", "''")
662
+ content_escaped = article[2].replace("'", "''")
663
+ category_escaped = article[3].replace("'", "''")
664
+ tags_escaped = article[4].replace("'", "''") if article[4] else ''
665
+
666
+ await client.execute(
667
+ f"""
668
+ INSERT INTO test_async_simple_query_articles (id, title, content, category, tags, author, published_date)
669
+ VALUES ({article[0]}, '{title_escaped}', '{content_escaped}', '{category_escaped}', '{tags_escaped}', '{article[5]}', '{article[6]}')
670
+ """
671
+ )
672
+
673
+ # Create fulltext index - use only the columns that simple_query will search
674
+ try:
675
+ await client.fulltext_index.create(
676
+ "test_async_simple_query_articles", "ft_async_tx_articles", ["title", "content"]
677
+ )
678
+ except Exception as e:
679
+ print(f"Fulltext index creation warning: {e}")
680
+
681
+ yield client, test_db
682
+
683
+ # Cleanup
684
+ try:
685
+ await client.execute(f"DROP DATABASE IF EXISTS {test_db}")
686
+ await client.disconnect()
687
+ except Exception as e:
688
+ print(f"Cleanup warning: {e}")
689
+
690
+ @pytest.mark.asyncio
691
+ async def test_async_transaction_simple_query_basic(self, async_tx_client_setup):
692
+ """Test basic async transaction simple query."""
693
+ client, test_db = async_tx_client_setup
694
+ async with client.transaction() as tx:
695
+ results = await tx.query(
696
+ AsyncArticle.title, AsyncArticle.content, boolean_match("title", "content").encourage("python")
697
+ ).execute()
698
+
699
+ rows = results.fetchall()
700
+ assert isinstance(rows, list)
701
+ # If no results, just assert that the query executed without error
702
+ if len(rows) > 0:
703
+ # Should find Python transaction related articles
704
+ found_python = any("Python" in row[1] for row in rows)
705
+ assert found_python
706
+ else:
707
+ # Query executed successfully even if no results
708
+ assert True
709
+
710
+ @pytest.mark.asyncio
711
+ async def test_async_transaction_simple_query_with_score(self, async_tx_client_setup):
712
+ """Test async transaction simple query with scoring."""
713
+ client, test_db = async_tx_client_setup
714
+ async with client.transaction() as tx:
715
+ results = await tx.query(
716
+ AsyncArticle.title,
717
+ AsyncArticle.content,
718
+ boolean_match("title", "content").encourage("python").label("score"),
719
+ ).execute()
720
+
721
+ rows = results.fetchall()
722
+ assert isinstance(rows, list)
723
+ # If no results, just assert that the query executed without error
724
+ if len(rows) > 0:
725
+ assert True # Found results
726
+ else:
727
+ assert True # Query executed successfully
728
+
729
+ # Check that score column is included
730
+ assert len(results.columns) == 3 # title, content, score
731
+ assert "score" in results.columns
732
+ score_column_index = results.columns.index("score")
733
+
734
+ # Verify score values are numeric (if we have results)
735
+ if len(rows) > 0:
736
+ for row in rows:
737
+ score = row[score_column_index]
738
+ assert isinstance(score, (int, float))
739
+ assert score >= 0
740
+
741
+ @pytest.mark.asyncio
742
+ async def test_async_transaction_simple_query_boolean(self, async_tx_client_setup):
743
+ """Test async transaction simple query with boolean mode."""
744
+ client, test_db = async_tx_client_setup
745
+ async with client.transaction() as tx:
746
+ results = await (
747
+ tx.query(
748
+ AsyncArticle,
749
+ )
750
+ .filter(
751
+ boolean_match("title", "content").must("transaction").must_not("deprecated"),
752
+ AsyncArticle.category == "Programming",
753
+ )
754
+ .execute()
755
+ )
756
+
757
+ rows = results.fetchall()
758
+ assert isinstance(rows, list)
759
+ # Should find transaction articles in Programming category without deprecated content
760
+ for row in rows:
761
+ title_content = f"{row[1]} {row[2]}".lower()
762
+ assert "transaction" in title_content
763
+ assert "deprecated" not in title_content
764
+ assert row[3] in ["Programming", "Database"]
765
+
766
+ @pytest.mark.asyncio
767
+ async def test_async_transaction_simple_query_complex(self, async_tx_client_setup):
768
+ """Test complex async transaction simple query with multiple conditions."""
769
+ client, test_db = async_tx_client_setup
770
+ async with client.transaction() as tx:
771
+ results = await (
772
+ tx.query(
773
+ AsyncArticle.title,
774
+ AsyncArticle.content,
775
+ boolean_match("title", "content").encourage("database").label("score"),
776
+ )
777
+ .filter(AsyncArticle.category.in_(["Programming", "Database"]))
778
+ .limit(3)
779
+ .execute()
780
+ )
781
+ rows = results.fetchall()
782
+ assert isinstance(rows, list)
783
+ assert len(rows) <= 3
784
+ # Check that we got results (columns: title, content, score)
785
+ for row in rows:
786
+ assert len(row) == 3 # title, content, score
787
+
788
+ @pytest.mark.asyncio
789
+ async def test_async_transaction_simple_query_error_handling(self, async_tx_client_setup):
790
+ """Test error handling in async transaction simple query."""
791
+ client, test_db = async_tx_client_setup
792
+ async with client.transaction() as tx:
793
+ # This query should execute successfully now
794
+ results = await tx.query(
795
+ AsyncArticle.title, AsyncArticle.content, boolean_match("title", "content").encourage("test")
796
+ ).execute()
797
+ assert isinstance(results.fetchall(), list)
798
+ # Query executed successfully
799
+
800
+
801
+ if __name__ == "__main__":
802
+ pytest.main([__file__, "-v"])