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,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"])
|