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