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,915 @@
|
|
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
|
+
Online tests for SimpleFulltextQueryBuilder.
|
17
|
+
|
18
|
+
Tests functionality against a real MatrixOne database to verify
|
19
|
+
that generated SQL works correctly and returns expected results.
|
20
|
+
"""
|
21
|
+
|
22
|
+
import pytest
|
23
|
+
from sqlalchemy import Column, Integer, String, Text
|
24
|
+
from sqlalchemy.orm import declarative_base
|
25
|
+
|
26
|
+
from matrixone.client import Client
|
27
|
+
from matrixone.config import get_connection_kwargs
|
28
|
+
from matrixone.exceptions import QueryError
|
29
|
+
from matrixone.sqlalchemy_ext import boolean_match
|
30
|
+
|
31
|
+
# SQLAlchemy model for testing
|
32
|
+
Base = declarative_base()
|
33
|
+
|
34
|
+
|
35
|
+
class Article(Base):
|
36
|
+
__tablename__ = "test_simple_fulltext_articles"
|
37
|
+
|
38
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
39
|
+
title = Column(String(255), nullable=False)
|
40
|
+
content = Column(Text, nullable=False)
|
41
|
+
category = Column(String(100), nullable=False)
|
42
|
+
tags = Column(String(500))
|
43
|
+
|
44
|
+
|
45
|
+
class TestSimpleFulltextOnline:
|
46
|
+
"""Online tests for SimpleFulltextQueryBuilder with real database."""
|
47
|
+
|
48
|
+
@classmethod
|
49
|
+
def setup_class(cls):
|
50
|
+
"""Set up test database and data."""
|
51
|
+
# Get connection parameters
|
52
|
+
conn_params = get_connection_kwargs()
|
53
|
+
client_params = {k: v for k, v in conn_params.items() if k in ['host', 'port', 'user', 'password', 'database']}
|
54
|
+
|
55
|
+
cls.client = Client()
|
56
|
+
cls.client.connect(**client_params)
|
57
|
+
|
58
|
+
cls.test_db = "test_simple_fulltext"
|
59
|
+
try:
|
60
|
+
cls.client.execute(f"DROP DATABASE IF EXISTS {cls.test_db}")
|
61
|
+
cls.client.execute(f"CREATE DATABASE {cls.test_db}")
|
62
|
+
cls.client.execute(f"USE {cls.test_db}")
|
63
|
+
except Exception as e:
|
64
|
+
print(f"Database setup warning: {e}")
|
65
|
+
|
66
|
+
# Create test table
|
67
|
+
cls.client.execute("DROP TABLE IF EXISTS test_simple_fulltext_articles")
|
68
|
+
cls.client.execute(
|
69
|
+
"""
|
70
|
+
CREATE TABLE IF NOT EXISTS test_simple_fulltext_articles (
|
71
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
72
|
+
title VARCHAR(255) NOT NULL,
|
73
|
+
content TEXT NOT NULL,
|
74
|
+
category VARCHAR(100) NOT NULL,
|
75
|
+
tags VARCHAR(500)
|
76
|
+
)
|
77
|
+
"""
|
78
|
+
)
|
79
|
+
|
80
|
+
# Insert test data
|
81
|
+
test_articles = [
|
82
|
+
(
|
83
|
+
1,
|
84
|
+
"Python Programming Tutorial",
|
85
|
+
"Learn Python programming from basics to advanced concepts",
|
86
|
+
"Programming",
|
87
|
+
"python,tutorial,beginner",
|
88
|
+
),
|
89
|
+
(
|
90
|
+
2,
|
91
|
+
"Machine Learning with Python",
|
92
|
+
"Complete guide to machine learning algorithms and implementation",
|
93
|
+
"AI",
|
94
|
+
"python,ml,ai,algorithms",
|
95
|
+
),
|
96
|
+
(
|
97
|
+
3,
|
98
|
+
"JavaScript Web Development",
|
99
|
+
"Modern JavaScript development for web applications",
|
100
|
+
"Programming",
|
101
|
+
"javascript,web,frontend",
|
102
|
+
),
|
103
|
+
(
|
104
|
+
4,
|
105
|
+
"Data Science Fundamentals",
|
106
|
+
"Introduction to data science concepts and tools",
|
107
|
+
"AI",
|
108
|
+
"data,science,analytics",
|
109
|
+
),
|
110
|
+
(
|
111
|
+
5,
|
112
|
+
"Deprecated Python Libraries",
|
113
|
+
"Old Python libraries that should be avoided",
|
114
|
+
"Programming",
|
115
|
+
"python,deprecated,legacy",
|
116
|
+
),
|
117
|
+
(
|
118
|
+
6,
|
119
|
+
"Advanced Machine Learning",
|
120
|
+
"Deep learning and neural networks with TensorFlow",
|
121
|
+
"AI",
|
122
|
+
"ai,deeplearning,tensorflow",
|
123
|
+
),
|
124
|
+
(
|
125
|
+
7,
|
126
|
+
"Web Development Best Practices",
|
127
|
+
"Modern practices for building web applications",
|
128
|
+
"Programming",
|
129
|
+
"web,best practices,modern",
|
130
|
+
),
|
131
|
+
(
|
132
|
+
8,
|
133
|
+
"Artificial Intelligence Overview",
|
134
|
+
"Introduction to AI concepts and applications",
|
135
|
+
"AI",
|
136
|
+
"ai,overview,introduction",
|
137
|
+
),
|
138
|
+
]
|
139
|
+
|
140
|
+
for article in test_articles:
|
141
|
+
title_escaped = article[1].replace("'", "''")
|
142
|
+
content_escaped = article[2].replace("'", "''")
|
143
|
+
category_escaped = article[3].replace("'", "''")
|
144
|
+
tags_escaped = article[4].replace("'", "''") if article[4] else ''
|
145
|
+
|
146
|
+
cls.client.execute(
|
147
|
+
f"""
|
148
|
+
INSERT INTO test_simple_fulltext_articles (id, title, content, category, tags)
|
149
|
+
VALUES ({article[0]}, '{title_escaped}', '{content_escaped}', '{category_escaped}', '{tags_escaped}')
|
150
|
+
"""
|
151
|
+
)
|
152
|
+
|
153
|
+
# Create fulltext index
|
154
|
+
try:
|
155
|
+
cls.client.execute("CREATE FULLTEXT INDEX ft_articles ON test_simple_fulltext_articles(title, content, tags)")
|
156
|
+
except Exception as e:
|
157
|
+
print(f"Fulltext index creation warning: {e}")
|
158
|
+
|
159
|
+
@classmethod
|
160
|
+
def teardown_class(cls):
|
161
|
+
"""Clean up test database."""
|
162
|
+
if hasattr(cls, 'client'):
|
163
|
+
cls.client.disconnect()
|
164
|
+
|
165
|
+
def test_basic_natural_language_search(self):
|
166
|
+
"""Test basic natural language search with real database."""
|
167
|
+
results = self.client.query(
|
168
|
+
Article.id,
|
169
|
+
Article.title,
|
170
|
+
Article.content,
|
171
|
+
Article.category,
|
172
|
+
Article.tags,
|
173
|
+
boolean_match("title", "content", "tags").encourage("python programming"),
|
174
|
+
).execute()
|
175
|
+
|
176
|
+
# Verify result structure
|
177
|
+
assert isinstance(results, object), "Results should be a result set object"
|
178
|
+
assert hasattr(results, 'fetchall'), "Results should have fetchall method"
|
179
|
+
assert hasattr(results, 'columns'), "Results should have columns attribute"
|
180
|
+
assert hasattr(results, 'rows'), "Results should have rows attribute"
|
181
|
+
|
182
|
+
# Verify columns are correct (including the MATCH function column)
|
183
|
+
expected_base_columns = ['id', 'title', 'content', 'category', 'tags']
|
184
|
+
assert len(results.columns) >= len(
|
185
|
+
expected_base_columns
|
186
|
+
), f"Should have at least {len(expected_base_columns)} columns, got {len(results.columns)}"
|
187
|
+
|
188
|
+
# Verify base columns are correct
|
189
|
+
for i, expected_col in enumerate(expected_base_columns):
|
190
|
+
assert results.columns[i] == expected_col, f"Column {i} should be '{expected_col}', got '{results.columns[i]}'"
|
191
|
+
|
192
|
+
# Verify there's an additional MATCH column (for boolean_match)
|
193
|
+
assert len(results.columns) > len(expected_base_columns), "Should have additional MATCH column from boolean_match"
|
194
|
+
match_column = results.columns[-1]
|
195
|
+
assert "MATCH" in match_column, f"Last column should contain MATCH function, got: {match_column}"
|
196
|
+
|
197
|
+
# Verify we have results
|
198
|
+
all_rows = results.fetchall()
|
199
|
+
assert isinstance(all_rows, list), "fetchall() should return a list"
|
200
|
+
assert len(all_rows) > 0, "Should find at least one article matching 'python programming'"
|
201
|
+
|
202
|
+
# Verify row structure
|
203
|
+
for i, row in enumerate(all_rows):
|
204
|
+
# Row can be tuple, list, or SQLAlchemy Row object
|
205
|
+
assert hasattr(row, '__getitem__'), f"Row {i} should support indexing, got {type(row)}"
|
206
|
+
assert hasattr(row, '__len__'), f"Row {i} should support len(), got {type(row)}"
|
207
|
+
assert len(row) == len(results.columns), f"Row {i} should have {len(results.columns)} columns, got {len(row)}"
|
208
|
+
|
209
|
+
# Verify content matches search criteria
|
210
|
+
found_python = False
|
211
|
+
found_programming = False
|
212
|
+
for row in all_rows:
|
213
|
+
# Column indices: 0=id, 1=title, 2=content, 3=category, 4=tags, 5=MATCH function
|
214
|
+
title = str(row[1]).lower() if row[1] else ""
|
215
|
+
content = str(row[2]).lower() if row[2] else ""
|
216
|
+
tags = str(row[4]).lower() if len(row) > 4 and row[4] else ""
|
217
|
+
|
218
|
+
if "python" in title or "python" in content or "python" in tags:
|
219
|
+
found_python = True
|
220
|
+
if "programming" in title or "programming" in content or "programming" in tags:
|
221
|
+
found_programming = True
|
222
|
+
|
223
|
+
assert found_python, "Should find articles containing 'python' in title, content, or tags"
|
224
|
+
assert found_programming, "Should find articles containing 'programming' in title, content, or tags"
|
225
|
+
|
226
|
+
def test_boolean_mode_required_terms(self):
|
227
|
+
"""Test boolean mode with required terms."""
|
228
|
+
results = self.client.query(
|
229
|
+
Article.id,
|
230
|
+
Article.title,
|
231
|
+
Article.content,
|
232
|
+
Article.category,
|
233
|
+
Article.tags,
|
234
|
+
boolean_match("title", "content", "tags").must("python", "tutorial"),
|
235
|
+
).execute()
|
236
|
+
|
237
|
+
assert isinstance(results.fetchall(), list)
|
238
|
+
# Should find articles that have both "python" AND "tutorial"
|
239
|
+
if results.fetchall():
|
240
|
+
for row in results.fetchall():
|
241
|
+
title_content = f"{row[1]} {row[2]}".lower() # title + content
|
242
|
+
assert "python" in title_content and "tutorial" in title_content
|
243
|
+
|
244
|
+
def test_boolean_mode_excluded_terms(self):
|
245
|
+
"""Test boolean mode with excluded terms."""
|
246
|
+
results = self.client.query(
|
247
|
+
Article.id,
|
248
|
+
Article.title,
|
249
|
+
Article.content,
|
250
|
+
Article.category,
|
251
|
+
Article.tags,
|
252
|
+
boolean_match("title", "content", "tags").must("python").must_not("deprecated"),
|
253
|
+
).execute()
|
254
|
+
|
255
|
+
assert isinstance(results.fetchall(), list)
|
256
|
+
# Should find Python articles but exclude deprecated ones
|
257
|
+
for row in results.fetchall():
|
258
|
+
title_content = f"{row[1]} {row[2]}".lower() # title + content
|
259
|
+
assert "python" in title_content
|
260
|
+
assert "deprecated" not in title_content
|
261
|
+
|
262
|
+
def test_search_with_score(self):
|
263
|
+
"""Test search with relevance scoring."""
|
264
|
+
results = self.client.query(
|
265
|
+
Article.id,
|
266
|
+
Article.title,
|
267
|
+
Article.content,
|
268
|
+
Article.category,
|
269
|
+
Article.tags,
|
270
|
+
boolean_match("title", "content", "tags").encourage("machine learning").label("score"),
|
271
|
+
).execute()
|
272
|
+
|
273
|
+
# Verify result structure
|
274
|
+
assert isinstance(results, object), "Results should be a result set object"
|
275
|
+
assert hasattr(results, 'fetchall'), "Results should have fetchall method"
|
276
|
+
assert hasattr(results, 'columns'), "Results should have columns attribute"
|
277
|
+
|
278
|
+
# Verify columns include score
|
279
|
+
expected_base_columns = ['id', 'title', 'content', 'category', 'tags']
|
280
|
+
assert len(results.columns) > len(
|
281
|
+
expected_base_columns
|
282
|
+
), f"Should have more than {len(expected_base_columns)} columns (including score)"
|
283
|
+
|
284
|
+
# Check if score column exists (it should be labeled as "score")
|
285
|
+
has_score_column = any('score' in col.lower() for col in results.columns)
|
286
|
+
assert has_score_column, f"Should have a score column. Found columns: {results.columns}"
|
287
|
+
|
288
|
+
# Verify we have results
|
289
|
+
all_rows = results.fetchall()
|
290
|
+
assert isinstance(all_rows, list), "fetchall() should return a list"
|
291
|
+
assert len(all_rows) > 0, "Should find at least one article matching 'machine learning'"
|
292
|
+
|
293
|
+
# Verify row structure
|
294
|
+
for i, row in enumerate(all_rows):
|
295
|
+
# Row can be tuple, list, or SQLAlchemy Row object
|
296
|
+
assert hasattr(row, '__getitem__'), f"Row {i} should support indexing, got {type(row)}"
|
297
|
+
assert hasattr(row, '__len__'), f"Row {i} should support len(), got {type(row)}"
|
298
|
+
assert len(row) >= len(
|
299
|
+
expected_base_columns
|
300
|
+
), f"Row {i} should have at least {len(expected_base_columns)} columns, got {len(row)}"
|
301
|
+
|
302
|
+
# Verify content matches search criteria
|
303
|
+
found_machine = False
|
304
|
+
found_learning = False
|
305
|
+
for row in all_rows:
|
306
|
+
title = str(row[1]).lower() if row[1] else ""
|
307
|
+
content = str(row[2]).lower() if row[2] else ""
|
308
|
+
tags = str(row[4]).lower() if len(row) > 4 and row[4] else ""
|
309
|
+
|
310
|
+
combined_text = f"{title} {content} {tags}"
|
311
|
+
if "machine" in combined_text:
|
312
|
+
found_machine = True
|
313
|
+
if "learning" in combined_text:
|
314
|
+
found_learning = True
|
315
|
+
|
316
|
+
assert found_machine, "Should find articles containing 'machine' in title, content, or tags"
|
317
|
+
assert found_learning, "Should find articles containing 'learning' in title, content, or tags"
|
318
|
+
|
319
|
+
# Verify score values are numeric (if score column exists)
|
320
|
+
score_column_index = None
|
321
|
+
for i, col in enumerate(results.columns):
|
322
|
+
if 'score' in col.lower():
|
323
|
+
score_column_index = i
|
324
|
+
break
|
325
|
+
|
326
|
+
if score_column_index is not None:
|
327
|
+
for i, row in enumerate(all_rows):
|
328
|
+
if len(row) > score_column_index:
|
329
|
+
score_value = row[score_column_index]
|
330
|
+
assert isinstance(
|
331
|
+
score_value, (int, float)
|
332
|
+
), f"Score value in row {i} should be numeric, got {type(score_value)}"
|
333
|
+
assert score_value >= 0, f"Score value in row {i} should be non-negative, got {score_value}"
|
334
|
+
|
335
|
+
def test_search_with_where_conditions(self):
|
336
|
+
"""Test search with additional WHERE conditions."""
|
337
|
+
results = (
|
338
|
+
self.client.query(
|
339
|
+
"test_simple_fulltext_articles.title",
|
340
|
+
"test_simple_fulltext_articles.content",
|
341
|
+
"test_simple_fulltext_articles.tags",
|
342
|
+
)
|
343
|
+
.filter(
|
344
|
+
boolean_match("title", "content", "tags").encourage("programming"),
|
345
|
+
"test_simple_fulltext_articles.category = 'Programming'",
|
346
|
+
)
|
347
|
+
.execute()
|
348
|
+
)
|
349
|
+
|
350
|
+
# Verify result structure
|
351
|
+
assert isinstance(results, object), "Results should be a result set object"
|
352
|
+
assert hasattr(results, 'rows'), "Results should have rows attribute"
|
353
|
+
assert hasattr(results, 'columns'), "Results should have columns attribute"
|
354
|
+
|
355
|
+
# Verify columns are correct (title, content, tags)
|
356
|
+
expected_columns = ['title', 'content', 'tags']
|
357
|
+
assert len(results.columns) == len(
|
358
|
+
expected_columns
|
359
|
+
), f"Expected {len(expected_columns)} columns, got {len(results.columns)}"
|
360
|
+
for i, expected_col in enumerate(expected_columns):
|
361
|
+
assert results.columns[i] == expected_col, f"Column {i} should be '{expected_col}', got '{results.columns[i]}'"
|
362
|
+
|
363
|
+
# Verify we have results
|
364
|
+
assert isinstance(results.rows, list), "Results.rows should be a list"
|
365
|
+
# Note: We can't assert len > 0 because there might not be Programming articles
|
366
|
+
|
367
|
+
# Verify row structure
|
368
|
+
for i, row in enumerate(results.rows):
|
369
|
+
# Row can be tuple, list, or SQLAlchemy Row object
|
370
|
+
assert hasattr(row, '__getitem__'), f"Row {i} should support indexing, got {type(row)}"
|
371
|
+
assert hasattr(row, '__len__'), f"Row {i} should support len(), got {type(row)}"
|
372
|
+
assert len(row) == len(expected_columns), f"Row {i} should have {len(expected_columns)} columns, got {len(row)}"
|
373
|
+
|
374
|
+
# Verify content matches search criteria (if we have results)
|
375
|
+
if results.rows:
|
376
|
+
found_programming = False
|
377
|
+
for row in results.rows:
|
378
|
+
title = str(row[0]).lower() if row[0] else ""
|
379
|
+
content = str(row[1]).lower() if row[1] else ""
|
380
|
+
tags = str(row[2]).lower() if row[2] else ""
|
381
|
+
|
382
|
+
combined_text = f"{title} {content} {tags}"
|
383
|
+
if "programming" in combined_text:
|
384
|
+
found_programming = True
|
385
|
+
|
386
|
+
assert found_programming, "Should find articles containing 'programming' in title, content, or tags"
|
387
|
+
|
388
|
+
def test_search_with_ordering_and_limit(self):
|
389
|
+
"""Test search with ordering and pagination."""
|
390
|
+
results = (
|
391
|
+
self.client.query(
|
392
|
+
"test_simple_fulltext_articles.title",
|
393
|
+
"test_simple_fulltext_articles.content",
|
394
|
+
"test_simple_fulltext_articles.tags",
|
395
|
+
boolean_match("title", "content", "tags").encourage("development").label("score"),
|
396
|
+
)
|
397
|
+
.order_by("score ASC")
|
398
|
+
.limit(3)
|
399
|
+
.execute()
|
400
|
+
)
|
401
|
+
|
402
|
+
# Verify result structure
|
403
|
+
assert isinstance(results, object), "Results should be a result set object"
|
404
|
+
assert hasattr(results, 'rows'), "Results should have rows attribute"
|
405
|
+
assert hasattr(results, 'columns'), "Results should have columns attribute"
|
406
|
+
|
407
|
+
# Verify columns include score
|
408
|
+
expected_columns = ['title', 'content', 'tags']
|
409
|
+
assert len(results.columns) > len(
|
410
|
+
expected_columns
|
411
|
+
), f"Should have more than {len(expected_columns)} columns (including score)"
|
412
|
+
|
413
|
+
# Check if score column exists
|
414
|
+
has_score_column = any('score' in col.lower() for col in results.columns)
|
415
|
+
assert has_score_column, f"Should have a score column. Found columns: {results.columns}"
|
416
|
+
|
417
|
+
# Verify limit is respected
|
418
|
+
assert isinstance(results.rows, list), "Results.rows should be a list"
|
419
|
+
assert len(results.rows) <= 3, f"Should respect limit of 3, got {len(results.rows)} results"
|
420
|
+
|
421
|
+
# Verify row structure
|
422
|
+
for i, row in enumerate(results.rows):
|
423
|
+
# Row can be tuple, list, or SQLAlchemy Row object
|
424
|
+
assert hasattr(row, '__getitem__'), f"Row {i} should support indexing, got {type(row)}"
|
425
|
+
assert hasattr(row, '__len__'), f"Row {i} should support len(), got {type(row)}"
|
426
|
+
assert len(row) >= len(
|
427
|
+
expected_columns
|
428
|
+
), f"Row {i} should have at least {len(expected_columns)} columns, got {len(row)}"
|
429
|
+
|
430
|
+
# Verify ordering by score (if we have multiple results)
|
431
|
+
if len(results.rows) > 1:
|
432
|
+
# Find score column index
|
433
|
+
score_column_index = None
|
434
|
+
for i, col in enumerate(results.columns):
|
435
|
+
if 'score' in col.lower():
|
436
|
+
score_column_index = i
|
437
|
+
break
|
438
|
+
|
439
|
+
if score_column_index is not None:
|
440
|
+
scores = []
|
441
|
+
for row in results.rows:
|
442
|
+
if len(row) > score_column_index:
|
443
|
+
score = row[score_column_index]
|
444
|
+
assert isinstance(score, (int, float)), f"Score should be numeric, got {type(score)}"
|
445
|
+
scores.append(score)
|
446
|
+
|
447
|
+
# Verify scores are in ascending order (ASC was specified)
|
448
|
+
assert scores == sorted(scores), f"Results should be ordered by score ASC. Got scores: {scores}"
|
449
|
+
|
450
|
+
# Verify content matches search criteria (if we have results)
|
451
|
+
if results.rows:
|
452
|
+
found_development = False
|
453
|
+
for row in results.rows:
|
454
|
+
title = str(row[0]).lower() if row[0] else ""
|
455
|
+
content = str(row[1]).lower() if row[1] else ""
|
456
|
+
tags = str(row[2]).lower() if row[2] else ""
|
457
|
+
|
458
|
+
combined_text = f"{title} {content} {tags}"
|
459
|
+
if "development" in combined_text:
|
460
|
+
found_development = True
|
461
|
+
|
462
|
+
assert found_development, "Should find articles containing 'development' in title, content, or tags"
|
463
|
+
|
464
|
+
def test_search_by_category(self):
|
465
|
+
"""Test filtering by category."""
|
466
|
+
# Search AI category
|
467
|
+
ai_results = (
|
468
|
+
self.client.query(
|
469
|
+
"test_simple_fulltext_articles.title",
|
470
|
+
"test_simple_fulltext_articles.content",
|
471
|
+
"test_simple_fulltext_articles.tags",
|
472
|
+
)
|
473
|
+
.filter(
|
474
|
+
boolean_match("title", "content", "tags").encourage("learning"),
|
475
|
+
"test_simple_fulltext_articles.category = 'AI'",
|
476
|
+
)
|
477
|
+
.execute()
|
478
|
+
)
|
479
|
+
|
480
|
+
# Search Programming category
|
481
|
+
prog_results = (
|
482
|
+
self.client.query(
|
483
|
+
"test_simple_fulltext_articles.title",
|
484
|
+
"test_simple_fulltext_articles.content",
|
485
|
+
"test_simple_fulltext_articles.tags",
|
486
|
+
)
|
487
|
+
.filter(
|
488
|
+
boolean_match("title", "content", "tags").encourage("programming"),
|
489
|
+
"test_simple_fulltext_articles.category = 'Programming'",
|
490
|
+
)
|
491
|
+
.execute()
|
492
|
+
)
|
493
|
+
|
494
|
+
# Verify result structures
|
495
|
+
for result_name, result in [("AI results", ai_results), ("Programming results", prog_results)]:
|
496
|
+
assert isinstance(result, object), f"{result_name} should be a result set object"
|
497
|
+
assert hasattr(result, 'rows'), f"{result_name} should have rows attribute"
|
498
|
+
assert hasattr(result, 'columns'), f"{result_name} should have columns attribute"
|
499
|
+
|
500
|
+
# Verify columns are correct
|
501
|
+
expected_columns = ['title', 'content', 'tags']
|
502
|
+
assert len(result.columns) == len(
|
503
|
+
expected_columns
|
504
|
+
), f"{result_name}: Expected {len(expected_columns)} columns, got {len(result.columns)}"
|
505
|
+
for i, expected_col in enumerate(expected_columns):
|
506
|
+
assert (
|
507
|
+
result.columns[i] == expected_col
|
508
|
+
), f"{result_name}: Column {i} should be '{expected_col}', got '{result.columns[i]}'"
|
509
|
+
|
510
|
+
# Verify rows structure
|
511
|
+
assert isinstance(result.rows, list), f"{result_name}: rows should be a list"
|
512
|
+
for i, row in enumerate(result.rows):
|
513
|
+
# Row can be tuple, list, or SQLAlchemy Row object
|
514
|
+
assert hasattr(row, '__getitem__'), f"{result_name}: Row {i} should support indexing, got {type(row)}"
|
515
|
+
assert hasattr(row, '__len__'), f"{result_name}: Row {i} should support len(), got {type(row)}"
|
516
|
+
assert len(row) == len(
|
517
|
+
expected_columns
|
518
|
+
), f"{result_name}: Row {i} should have {len(expected_columns)} columns, got {len(row)}"
|
519
|
+
|
520
|
+
# Verify content matches search criteria (if we have results)
|
521
|
+
if ai_results.rows:
|
522
|
+
found_learning = False
|
523
|
+
for row in ai_results.rows:
|
524
|
+
title = str(row[0]).lower() if row[0] else ""
|
525
|
+
content = str(row[1]).lower() if row[1] else ""
|
526
|
+
tags = str(row[2]).lower() if row[2] else ""
|
527
|
+
|
528
|
+
combined_text = f"{title} {content} {tags}"
|
529
|
+
if "learning" in combined_text:
|
530
|
+
found_learning = True
|
531
|
+
|
532
|
+
assert found_learning, "AI results should contain articles with 'learning' in title, content, or tags"
|
533
|
+
|
534
|
+
if prog_results.rows:
|
535
|
+
found_programming = False
|
536
|
+
for row in prog_results.rows:
|
537
|
+
title = str(row[0]).lower() if row[0] else ""
|
538
|
+
content = str(row[1]).lower() if row[1] else ""
|
539
|
+
tags = str(row[2]).lower() if row[2] else ""
|
540
|
+
|
541
|
+
combined_text = f"{title} {content} {tags}"
|
542
|
+
if "programming" in combined_text:
|
543
|
+
found_programming = True
|
544
|
+
|
545
|
+
assert (
|
546
|
+
found_programming
|
547
|
+
), "Programming results should contain articles with 'programming' in title, content, or tags"
|
548
|
+
|
549
|
+
def test_complex_boolean_search(self):
|
550
|
+
"""Test complex boolean search with multiple terms."""
|
551
|
+
results = (
|
552
|
+
self.client.query(
|
553
|
+
"test_simple_fulltext_articles.title",
|
554
|
+
"test_simple_fulltext_articles.content",
|
555
|
+
"test_simple_fulltext_articles.tags",
|
556
|
+
)
|
557
|
+
.filter(
|
558
|
+
boolean_match("title", "content", "tags").must("web").must_not("deprecated", "legacy"),
|
559
|
+
"test_simple_fulltext_articles.category = 'Programming'",
|
560
|
+
)
|
561
|
+
.execute()
|
562
|
+
)
|
563
|
+
|
564
|
+
# Verify result structure
|
565
|
+
assert isinstance(results, object), "Results should be a result set object"
|
566
|
+
assert hasattr(results, 'rows'), "Results should have rows attribute"
|
567
|
+
assert hasattr(results, 'columns'), "Results should have columns attribute"
|
568
|
+
|
569
|
+
# Verify columns are correct
|
570
|
+
expected_columns = ['title', 'content', 'tags']
|
571
|
+
assert len(results.columns) == len(
|
572
|
+
expected_columns
|
573
|
+
), f"Expected {len(expected_columns)} columns, got {len(results.columns)}"
|
574
|
+
for i, expected_col in enumerate(expected_columns):
|
575
|
+
assert results.columns[i] == expected_col, f"Column {i} should be '{expected_col}', got '{results.columns[i]}'"
|
576
|
+
|
577
|
+
# Verify we have results
|
578
|
+
assert isinstance(results.rows, list), "Results.rows should be a list"
|
579
|
+
# Note: We can't assert len > 0 because there might not be matching articles
|
580
|
+
|
581
|
+
# Verify row structure
|
582
|
+
for i, row in enumerate(results.rows):
|
583
|
+
# Row can be tuple, list, or SQLAlchemy Row object
|
584
|
+
assert hasattr(row, '__getitem__'), f"Row {i} should support indexing, got {type(row)}"
|
585
|
+
assert hasattr(row, '__len__'), f"Row {i} should support len(), got {type(row)}"
|
586
|
+
assert len(row) == len(expected_columns), f"Row {i} should have {len(expected_columns)} columns, got {len(row)}"
|
587
|
+
|
588
|
+
# Verify content matches complex boolean criteria (if we have results)
|
589
|
+
if results.rows:
|
590
|
+
for i, row in enumerate(results.rows):
|
591
|
+
title = str(row[0]).lower() if row[0] else ""
|
592
|
+
content = str(row[1]).lower() if row[1] else ""
|
593
|
+
tags = str(row[2]).lower() if row[2] else ""
|
594
|
+
|
595
|
+
combined_text = f"{title} {content} {tags}"
|
596
|
+
|
597
|
+
# Must have "web"
|
598
|
+
assert (
|
599
|
+
"web" in combined_text
|
600
|
+
), f"Row {i} should contain 'web' in title, content, or tags. Got: {combined_text[:100]}..."
|
601
|
+
|
602
|
+
# Must not have "deprecated"
|
603
|
+
assert (
|
604
|
+
"deprecated" not in combined_text
|
605
|
+
), f"Row {i} should not contain 'deprecated'. Got: {combined_text[:100]}..."
|
606
|
+
|
607
|
+
# Must not have "legacy"
|
608
|
+
assert "legacy" not in combined_text, f"Row {i} should not contain 'legacy'. Got: {combined_text[:100]}..."
|
609
|
+
|
610
|
+
# Category verification is handled by WHERE clause
|
611
|
+
|
612
|
+
def test_search_with_custom_score_alias(self):
|
613
|
+
"""Test search with custom score alias."""
|
614
|
+
results = (
|
615
|
+
self.client.query(
|
616
|
+
"test_simple_fulltext_articles.title",
|
617
|
+
"test_simple_fulltext_articles.content",
|
618
|
+
"test_simple_fulltext_articles.tags",
|
619
|
+
boolean_match("title", "content", "tags").encourage("artificial intelligence").label("relevance"),
|
620
|
+
)
|
621
|
+
.order_by("relevance ASC")
|
622
|
+
.execute()
|
623
|
+
)
|
624
|
+
|
625
|
+
assert isinstance(results.rows, list)
|
626
|
+
# Score column should be present (custom alias doesn't change result structure)
|
627
|
+
if results.rows:
|
628
|
+
score_index = len(results.columns) - 1
|
629
|
+
for row in results.rows:
|
630
|
+
score = row[score_index]
|
631
|
+
assert isinstance(score, (int, float))
|
632
|
+
|
633
|
+
def test_empty_search_results(self):
|
634
|
+
"""Test search that should return no results."""
|
635
|
+
results = self.client.query(
|
636
|
+
"test_simple_fulltext_articles.title",
|
637
|
+
"test_simple_fulltext_articles.content",
|
638
|
+
"test_simple_fulltext_articles.tags",
|
639
|
+
boolean_match("title", "content", "tags").encourage("nonexistent_term_xyz123"),
|
640
|
+
).execute()
|
641
|
+
|
642
|
+
assert isinstance(results.rows, list)
|
643
|
+
assert len(results.rows) == 0
|
644
|
+
|
645
|
+
def test_chinese_text_search(self):
|
646
|
+
"""Test search with non-English text."""
|
647
|
+
# Insert a Chinese article for testing
|
648
|
+
self.client.execute(
|
649
|
+
"""
|
650
|
+
INSERT INTO test_simple_fulltext_articles (title, content, category, tags)
|
651
|
+
VALUES ('中文测试', '这是一个中文全文搜索测试', 'Test', 'chinese,test')
|
652
|
+
"""
|
653
|
+
)
|
654
|
+
|
655
|
+
results = self.client.query(
|
656
|
+
"test_simple_fulltext_articles.title",
|
657
|
+
"test_simple_fulltext_articles.content",
|
658
|
+
"test_simple_fulltext_articles.tags",
|
659
|
+
boolean_match("title", "content", "tags").encourage("中文"),
|
660
|
+
).execute()
|
661
|
+
|
662
|
+
assert isinstance(results.rows, list)
|
663
|
+
# Should find the Chinese article
|
664
|
+
if results.rows:
|
665
|
+
found_chinese = any("中文" in row[1] for row in results.rows)
|
666
|
+
assert found_chinese
|
667
|
+
|
668
|
+
def test_error_handling_invalid_table(self):
|
669
|
+
"""Test error handling for invalid table."""
|
670
|
+
with pytest.raises(QueryError):
|
671
|
+
(
|
672
|
+
self.client.query(
|
673
|
+
"nonexistent_table.title",
|
674
|
+
"nonexistent_table.content",
|
675
|
+
boolean_match("title", "content").encourage("test"),
|
676
|
+
).execute()
|
677
|
+
)
|
678
|
+
|
679
|
+
def test_method_chaining_completeness(self):
|
680
|
+
"""Test that all methods return self for proper chaining."""
|
681
|
+
# Test method chaining with query builder
|
682
|
+
result = (
|
683
|
+
self.client.query("test_simple_fulltext_articles.title", "test_simple_fulltext_articles.content")
|
684
|
+
.filter(boolean_match("title", "content", "tags").encourage("test"))
|
685
|
+
.limit(5)
|
686
|
+
.execute()
|
687
|
+
)
|
688
|
+
|
689
|
+
# Verify chaining works
|
690
|
+
assert result is not None
|
691
|
+
|
692
|
+
|
693
|
+
class TestSimpleFulltextMigration:
|
694
|
+
"""Test migration from old fulltext interfaces to simple_query."""
|
695
|
+
|
696
|
+
@classmethod
|
697
|
+
def setup_class(cls):
|
698
|
+
"""Set up test database."""
|
699
|
+
conn_params = get_connection_kwargs()
|
700
|
+
client_params = {k: v for k, v in conn_params.items() if k in ['host', 'port', 'user', 'password', 'database']}
|
701
|
+
|
702
|
+
cls.client = Client()
|
703
|
+
cls.client.connect(**client_params)
|
704
|
+
|
705
|
+
cls.test_db = "test_migration"
|
706
|
+
try:
|
707
|
+
cls.client.execute(f"DROP DATABASE IF EXISTS {cls.test_db}")
|
708
|
+
cls.client.execute(f"CREATE DATABASE {cls.test_db}")
|
709
|
+
cls.client.execute(f"USE {cls.test_db}")
|
710
|
+
except Exception as e:
|
711
|
+
print(f"Database setup warning: {e}")
|
712
|
+
|
713
|
+
# Create test table
|
714
|
+
cls.client.execute("DROP TABLE IF EXISTS migration_test")
|
715
|
+
cls.client.execute(
|
716
|
+
"""
|
717
|
+
CREATE TABLE IF NOT EXISTS migration_test (
|
718
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
719
|
+
title VARCHAR(255) NOT NULL,
|
720
|
+
body TEXT NOT NULL
|
721
|
+
)
|
722
|
+
"""
|
723
|
+
)
|
724
|
+
|
725
|
+
# Insert test data
|
726
|
+
cls.client.execute(
|
727
|
+
"""
|
728
|
+
INSERT INTO migration_test (title, body) VALUES
|
729
|
+
('Python Tutorial', 'Learn Python programming'),
|
730
|
+
('Java Guide', 'Complete Java development guide'),
|
731
|
+
('Machine Learning', 'AI and ML concepts')
|
732
|
+
"""
|
733
|
+
)
|
734
|
+
|
735
|
+
# Create fulltext index
|
736
|
+
try:
|
737
|
+
cls.client.execute("CREATE FULLTEXT INDEX ft_migration ON migration_test(title, body)")
|
738
|
+
except Exception as e:
|
739
|
+
print(f"Fulltext index creation warning: {e}")
|
740
|
+
|
741
|
+
@classmethod
|
742
|
+
def teardown_class(cls):
|
743
|
+
"""Clean up."""
|
744
|
+
if hasattr(cls, 'client'):
|
745
|
+
cls.client.disconnect()
|
746
|
+
|
747
|
+
def test_replace_basic_fulltext_search(self):
|
748
|
+
"""Test replacing basic fulltext search with simple_query."""
|
749
|
+
# Old way (if it existed): complex builder setup
|
750
|
+
# New way: simple_query interface
|
751
|
+
results = self.client.query(
|
752
|
+
"migration_test.title", "migration_test.body", boolean_match("title", "body").encourage("python")
|
753
|
+
).execute()
|
754
|
+
|
755
|
+
assert isinstance(results.rows, list)
|
756
|
+
if results.rows:
|
757
|
+
found_python = any("Python" in row[1] for row in results.rows)
|
758
|
+
assert found_python
|
759
|
+
|
760
|
+
def test_replace_scored_search(self):
|
761
|
+
"""Test replacing scored fulltext search."""
|
762
|
+
# New way with scoring
|
763
|
+
results = (
|
764
|
+
self.client.query(
|
765
|
+
"migration_test.title",
|
766
|
+
"migration_test.body",
|
767
|
+
boolean_match("title", "body").encourage("programming").label("score"),
|
768
|
+
)
|
769
|
+
.order_by("score ASC")
|
770
|
+
.execute()
|
771
|
+
)
|
772
|
+
|
773
|
+
assert isinstance(results.rows, list)
|
774
|
+
# Verify score column is included
|
775
|
+
if results.rows:
|
776
|
+
assert len(results.columns) > 2 # Original columns + score
|
777
|
+
|
778
|
+
def test_replace_complex_search(self):
|
779
|
+
"""Test replacing complex fulltext search scenarios."""
|
780
|
+
# Boolean search with filtering
|
781
|
+
results = (
|
782
|
+
self.client.query(
|
783
|
+
"migration_test.title",
|
784
|
+
"migration_test.body",
|
785
|
+
boolean_match("title", "body").must("guide").must_not("deprecated").label("score"),
|
786
|
+
)
|
787
|
+
.limit(5)
|
788
|
+
.execute()
|
789
|
+
)
|
790
|
+
|
791
|
+
assert isinstance(results.rows, list)
|
792
|
+
# Should work without errors and return relevant results
|
793
|
+
|
794
|
+
@pytest.mark.asyncio
|
795
|
+
async def test_async_execute_functionality(self):
|
796
|
+
"""Test async_execute method with real database."""
|
797
|
+
from matrixone.async_client import AsyncClient
|
798
|
+
from matrixone.config import get_connection_kwargs
|
799
|
+
|
800
|
+
# Get connection parameters
|
801
|
+
conn_params = get_connection_kwargs()
|
802
|
+
client_params = {k: v for k, v in conn_params.items() if k in ['host', 'port', 'user', 'password', 'database']}
|
803
|
+
|
804
|
+
async_client = AsyncClient()
|
805
|
+
await async_client.connect(**client_params)
|
806
|
+
|
807
|
+
try:
|
808
|
+
# Use the same test database
|
809
|
+
await async_client.execute(f"USE {self.test_db}")
|
810
|
+
|
811
|
+
# Test async execute
|
812
|
+
results = (
|
813
|
+
await async_client.query(
|
814
|
+
"migration_test.title",
|
815
|
+
"migration_test.body",
|
816
|
+
boolean_match("title", "body").encourage("python").label("score"),
|
817
|
+
)
|
818
|
+
.limit(3)
|
819
|
+
.execute()
|
820
|
+
)
|
821
|
+
|
822
|
+
assert results is not None
|
823
|
+
assert hasattr(results, 'rows')
|
824
|
+
assert isinstance(results.rows, list)
|
825
|
+
|
826
|
+
finally:
|
827
|
+
await async_client.disconnect()
|
828
|
+
|
829
|
+
def test_transaction_simple_query_functionality(self):
|
830
|
+
"""Test TransactionSimpleFulltextQueryBuilder with real database."""
|
831
|
+
# Test within a transaction
|
832
|
+
with self.client.transaction() as tx:
|
833
|
+
results = (
|
834
|
+
tx.query(
|
835
|
+
"migration_test.title",
|
836
|
+
"migration_test.body",
|
837
|
+
boolean_match("title", "body").encourage("guide").label("score"),
|
838
|
+
)
|
839
|
+
.limit(3)
|
840
|
+
.execute()
|
841
|
+
)
|
842
|
+
|
843
|
+
assert results is not None
|
844
|
+
assert hasattr(results, 'rows')
|
845
|
+
assert isinstance(results.rows, list)
|
846
|
+
|
847
|
+
@pytest.mark.asyncio
|
848
|
+
async def test_async_transaction_simple_query_functionality(self):
|
849
|
+
"""Test AsyncTransactionSimpleFulltextQueryBuilder with real database."""
|
850
|
+
from matrixone.async_client import AsyncClient
|
851
|
+
from matrixone.config import get_connection_kwargs
|
852
|
+
|
853
|
+
# Get connection parameters
|
854
|
+
conn_params = get_connection_kwargs()
|
855
|
+
client_params = {k: v for k, v in conn_params.items() if k in ['host', 'port', 'user', 'password', 'database']}
|
856
|
+
|
857
|
+
async_client = AsyncClient()
|
858
|
+
await async_client.connect(**client_params)
|
859
|
+
|
860
|
+
try:
|
861
|
+
# Use the same test database
|
862
|
+
await async_client.execute(f"USE {self.test_db}")
|
863
|
+
|
864
|
+
# Test within an async transaction
|
865
|
+
async with async_client.transaction() as tx:
|
866
|
+
results = (
|
867
|
+
await tx.query(
|
868
|
+
"migration_test.title",
|
869
|
+
"migration_test.body",
|
870
|
+
boolean_match("title", "body").encourage("python").label("score"),
|
871
|
+
)
|
872
|
+
.limit(3)
|
873
|
+
.execute()
|
874
|
+
)
|
875
|
+
|
876
|
+
assert results is not None
|
877
|
+
assert hasattr(results, 'rows')
|
878
|
+
assert isinstance(results.rows, list)
|
879
|
+
|
880
|
+
finally:
|
881
|
+
await async_client.disconnect()
|
882
|
+
|
883
|
+
def test_error_handling_async_execute_with_sync_client(self):
|
884
|
+
"""Test that execute works with sync client (should not raise error)."""
|
885
|
+
# This should not raise an error - query should be available
|
886
|
+
result = (
|
887
|
+
self.client.query("migration_test.title", "migration_test.body")
|
888
|
+
.filter(boolean_match("title", "body").encourage("test"))
|
889
|
+
.execute()
|
890
|
+
)
|
891
|
+
|
892
|
+
# Test that query method works correctly
|
893
|
+
assert result is not None, "Query should return a result set"
|
894
|
+
|
895
|
+
@pytest.mark.asyncio
|
896
|
+
async def test_error_handling_execute_with_async_client(self):
|
897
|
+
"""Test that async query works correctly."""
|
898
|
+
from matrixone.async_client import AsyncClient, AsyncFulltextIndexManager
|
899
|
+
import asyncio
|
900
|
+
|
901
|
+
async_client = AsyncClient()
|
902
|
+
async_client._fulltext_index = AsyncFulltextIndexManager(async_client)
|
903
|
+
|
904
|
+
# Test that query method is available (without actually executing)
|
905
|
+
query_builder = async_client.query("test_table.title", "test_table.body").filter(
|
906
|
+
boolean_match("title", "body").encourage("test")
|
907
|
+
)
|
908
|
+
|
909
|
+
# Should return a query builder
|
910
|
+
assert query_builder is not None, "Query builder should be created"
|
911
|
+
print("✅ query builder created as expected for async client")
|
912
|
+
|
913
|
+
|
914
|
+
if __name__ == "__main__":
|
915
|
+
pytest.main([__file__, "-v"])
|