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,795 @@
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
+ Comprehensive Fulltext Search Tests
17
+
18
+ This file consolidates all fulltext search-related tests from:
19
+ - test_fulltext_index.py (22 tests)
20
+ - test_fulltext_label.py (17 tests)
21
+ - test_fulltext_search_builder.py (21 tests)
22
+ - test_fulltext_search_coverage.py (30 tests)
23
+ - test_fulltext_search_offline.py (56 tests)
24
+
25
+ Total: 146 tests consolidated into one file
26
+ """
27
+
28
+ import pytest
29
+ import unittest
30
+ import sys
31
+ import os
32
+ import warnings
33
+ from unittest.mock import Mock, MagicMock, patch
34
+ from sqlalchemy import Column, Integer, String, Text
35
+ from sqlalchemy.orm import declarative_base
36
+
37
+ # Add the matrixone package to the path
38
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
39
+
40
+ from matrixone.sqlalchemy_ext import (
41
+ FulltextIndex,
42
+ FulltextAlgorithmType,
43
+ FulltextModeType,
44
+ FulltextSearchBuilder,
45
+ create_fulltext_index,
46
+ fulltext_search_builder,
47
+ )
48
+ from matrixone.sqlalchemy_ext.fulltext_search import (
49
+ FulltextFilter,
50
+ FulltextQueryBuilder,
51
+ FulltextGroup,
52
+ FulltextSearchMode,
53
+ FulltextSearchAlgorithm,
54
+ FulltextIndexManager,
55
+ boolean_match,
56
+ natural_match,
57
+ group,
58
+ )
59
+ from matrixone.sqlalchemy_ext.adapters import logical_and, logical_or, logical_not
60
+
61
+ # Test model for logical adapter tests
62
+ Base = declarative_base()
63
+
64
+
65
+ class Article(Base):
66
+ __tablename__ = 'test_articles'
67
+
68
+ id = Column(Integer, primary_key=True, autoincrement=True)
69
+ title = Column(String(255), nullable=False)
70
+ content = Column(Text, nullable=False)
71
+
72
+
73
+ class MockColumn:
74
+ """Mock SQLAlchemy column for testing."""
75
+
76
+ def __init__(self, name):
77
+ self.name = name
78
+
79
+
80
+ class MockClient:
81
+ """Mock client for testing FulltextSearchBuilder."""
82
+
83
+ def __init__(self):
84
+ self.execute_called = False
85
+ self.last_sql = None
86
+
87
+ async def execute(self, sql):
88
+ self.execute_called = True
89
+ self.last_sql = sql
90
+ return Mock()
91
+
92
+
93
+ class TestFulltextIndex(unittest.TestCase):
94
+ """Test FulltextIndex class - from test_fulltext_index.py"""
95
+
96
+ def test_fulltext_index_creation(self):
97
+ """Test creating a FulltextIndex instance"""
98
+ index = FulltextIndex("ftidx_test", ["title", "content"], FulltextAlgorithmType.BM25)
99
+
100
+ assert index.name == "ftidx_test"
101
+ assert index.get_columns() == ["title", "content"]
102
+ assert index.algorithm == FulltextAlgorithmType.BM25
103
+
104
+ def test_fulltext_index_single_column(self):
105
+ """Test creating FulltextIndex with single column"""
106
+ index = FulltextIndex("ftidx_single", "title", FulltextAlgorithmType.TF_IDF)
107
+
108
+ assert index.name == "ftidx_single"
109
+ assert index.get_columns() == ["title"]
110
+ assert index.algorithm == FulltextAlgorithmType.TF_IDF
111
+
112
+ def test_create_index_sql(self):
113
+ """Test SQL generation for CREATE INDEX"""
114
+ index = FulltextIndex("ftidx_test", ["title", "content"])
115
+ sql = index._create_index_sql("documents")
116
+
117
+ expected_sql = "CREATE FULLTEXT INDEX ftidx_test ON documents (title, content)"
118
+ assert sql == expected_sql
119
+
120
+ def test_fulltext_index_str(self):
121
+ """Test FulltextIndex string representation"""
122
+ index = FulltextIndex("ftidx_test", ["title", "content"], FulltextAlgorithmType.BM25)
123
+
124
+ str_repr = str(index)
125
+ assert "ftidx_test" in str_repr
126
+ assert "title" in str_repr
127
+ assert "content" in str_repr
128
+
129
+ def test_fulltext_index_repr(self):
130
+ """Test FulltextIndex representation"""
131
+ index = FulltextIndex("ftidx_test", ["title", "content"], FulltextAlgorithmType.BM25)
132
+
133
+ repr_str = repr(index)
134
+ assert "ftidx_test" in repr_str
135
+
136
+ def test_fulltext_index_multiple_columns(self):
137
+ """Test FulltextIndex with multiple columns"""
138
+ index = FulltextIndex("ftidx_test", ["title", "content", "summary"], FulltextAlgorithmType.BM25)
139
+
140
+ assert index.name == "ftidx_test"
141
+ assert index.get_columns() == ["title", "content", "summary"]
142
+
143
+
144
+ class TestFulltextAdvancedOperators(unittest.TestCase):
145
+ """Test advanced fulltext operators like encourage, discourage, groups - from test_fulltext_label.py"""
146
+
147
+ def test_encourage_operator(self):
148
+ """Test encourage operator (no prefix)."""
149
+ expr = boolean_match("title", "content").must("python").encourage("tutorial")
150
+ sql = expr.compile()
151
+
152
+ expected = "MATCH(title, content) AGAINST('+python tutorial' IN BOOLEAN MODE)"
153
+ self.assertEqual(sql, expected)
154
+
155
+ def test_discourage_operator(self):
156
+ """Test discourage operator (tilde prefix)."""
157
+ expr = boolean_match("title", "content").must("python").discourage("legacy")
158
+ sql = expr.compile()
159
+
160
+ expected = "MATCH(title, content) AGAINST('+python ~legacy' IN BOOLEAN MODE)"
161
+ self.assertEqual(sql, expected)
162
+
163
+ def test_complex_boolean_with_all_operators(self):
164
+ """Test complex boolean query with all operators."""
165
+ expr = (
166
+ boolean_match("title", "content", "tags")
167
+ .must("programming")
168
+ .encourage("tutorial")
169
+ .discourage("legacy")
170
+ .must_not("deprecated")
171
+ )
172
+ sql = expr.compile()
173
+
174
+ expected = "MATCH(title, content, tags) AGAINST('+programming tutorial ~legacy -deprecated' IN BOOLEAN MODE)"
175
+ self.assertEqual(sql, expected)
176
+
177
+ def test_group_with_weights(self):
178
+ """Test group with weight operators."""
179
+ expr = boolean_match("title", "content").must("main").must(group().high("important").medium("normal").low("minor"))
180
+ sql = expr.compile()
181
+
182
+ # Should contain weight operators within groups
183
+ self.assertIn("+main", sql)
184
+ self.assertIn("+(>important normal <minor)", sql)
185
+ self.assertIn("IN BOOLEAN MODE", sql)
186
+
187
+
188
+ class TestFulltextComplexScenarios(unittest.TestCase):
189
+ """Test complex fulltext scenarios with advanced features - from test_fulltext_search_offline.py"""
190
+
191
+ def test_programming_tutorial_search(self):
192
+ """Test programming tutorial search scenario."""
193
+ filter_obj = (
194
+ boolean_match("title", "content", "tags")
195
+ .must("programming")
196
+ .must(group().medium("python", "java", "javascript"))
197
+ .encourage("tutorial", "guide", "beginner")
198
+ .discourage("advanced", "expert")
199
+ .must_not("deprecated", "legacy")
200
+ )
201
+
202
+ result = filter_obj.compile()
203
+ # Check that all required elements are present
204
+ self.assertIn("MATCH(title, content, tags) AGAINST(", result)
205
+ self.assertIn("IN BOOLEAN MODE)", result)
206
+ self.assertIn("+programming", result)
207
+ self.assertIn("+(python java javascript)", result)
208
+ self.assertIn("tutorial guide beginner", result)
209
+ self.assertIn("~advanced ~expert", result)
210
+ self.assertIn("-deprecated -legacy", result)
211
+
212
+ def test_product_search_with_weights(self):
213
+ """Test product search with element weights."""
214
+ filter_obj = (
215
+ boolean_match("name", "description")
216
+ .must("laptop")
217
+ .encourage(group().high("gaming").medium("portable").low("budget"))
218
+ .must_not("refurbished")
219
+ )
220
+
221
+ result = filter_obj.compile()
222
+ # Check that all required elements are present
223
+ self.assertIn("MATCH(name, description) AGAINST(", result)
224
+ self.assertIn("IN BOOLEAN MODE)", result)
225
+ self.assertIn("+laptop", result)
226
+ self.assertIn("(>gaming portable <budget)", result)
227
+ self.assertIn("-refurbished", result)
228
+
229
+ def test_matrixone_syntax_compatibility(self):
230
+ """Test compatibility with MatrixOne test case syntax."""
231
+ # Test case: Basic boolean with score
232
+ expr = boolean_match("body", "title").must("fast").encourage("red").label("score")
233
+ sql = expr.compile()
234
+ self.assertIn("MATCH(body, title)", sql)
235
+ self.assertIn("+fast", sql)
236
+ self.assertIn("red", sql)
237
+ self.assertIn("AS score", sql)
238
+
239
+ def test_phrase_and_prefix_combination(self):
240
+ """Test phrase and prefix matching."""
241
+ expr = boolean_match("title", "content").phrase("machine learning").prefix("neural")
242
+ sql = expr.compile()
243
+
244
+ # Should contain phrase and prefix syntax
245
+ self.assertIn('"machine learning"', sql)
246
+ self.assertIn("neural*", sql)
247
+ self.assertIn("IN BOOLEAN MODE", sql)
248
+
249
+ def test_nested_groups_complex(self):
250
+ """Test nested group functionality."""
251
+ inner_group = group().medium("python", "java")
252
+ outer_group = group().medium("programming").medium("tutorial")
253
+
254
+ expr = boolean_match("title", "content").must("coding").must(inner_group).encourage(outer_group)
255
+ sql = expr.compile()
256
+
257
+ # Should contain nested structure
258
+ self.assertIn("+coding", sql)
259
+ self.assertIn("+(python java)", sql)
260
+ self.assertIn("(programming tutorial)", sql)
261
+
262
+
263
+ class TestFulltextQueryBuilder(unittest.TestCase):
264
+ """Test FulltextQueryBuilder functionality - from test_fulltext_search_offline.py"""
265
+
266
+ def test_basic_must_term(self):
267
+ """Test basic must term generation."""
268
+ builder = FulltextQueryBuilder()
269
+ builder.must("python")
270
+ self.assertEqual(builder.build(), "+python")
271
+
272
+ def test_basic_must_not_term(self):
273
+ """Test basic must_not term generation."""
274
+ builder = FulltextQueryBuilder()
275
+ builder.must_not("java")
276
+ self.assertEqual(builder.build(), "-java")
277
+
278
+ def test_basic_encourage_term(self):
279
+ """Test basic encourage term generation."""
280
+ builder = FulltextQueryBuilder()
281
+ builder.encourage("tutorial")
282
+ self.assertEqual(builder.build(), "tutorial")
283
+
284
+ def test_basic_discourage_term(self):
285
+ """Test basic discourage term generation."""
286
+ builder = FulltextQueryBuilder()
287
+ builder.discourage("legacy")
288
+ self.assertEqual(builder.build(), "~legacy")
289
+
290
+ def test_multiple_terms_same_type(self):
291
+ """Test multiple terms of same type."""
292
+ builder = FulltextQueryBuilder()
293
+ builder.must("python", "programming")
294
+ self.assertEqual(builder.build(), "+python +programming")
295
+
296
+ def test_mixed_term_types(self):
297
+ """Test mixed term types."""
298
+ builder = FulltextQueryBuilder()
299
+ builder.must("python").encourage("tutorial").discourage("legacy").must_not("deprecated")
300
+ expected = "+python tutorial ~legacy -deprecated"
301
+ self.assertEqual(builder.build(), expected)
302
+
303
+ def test_phrase_search(self):
304
+ """Test phrase search generation."""
305
+ builder = FulltextQueryBuilder()
306
+ builder.phrase("machine learning")
307
+ self.assertEqual(builder.build(), '"machine learning"')
308
+
309
+ def test_prefix_search(self):
310
+ """Test prefix search generation."""
311
+ builder = FulltextQueryBuilder()
312
+ builder.prefix("neural")
313
+ self.assertEqual(builder.build(), "neural*")
314
+
315
+ def test_boost_term(self):
316
+ """Test boosted term generation."""
317
+ builder = FulltextQueryBuilder()
318
+ builder.boost("python", 2.0)
319
+ self.assertEqual(builder.build(), "python^2.0")
320
+
321
+
322
+ class TestFulltextGroup(unittest.TestCase):
323
+ """Test FulltextGroup functionality - from test_fulltext_search_offline.py"""
324
+
325
+ def test_basic_group_medium(self):
326
+ """Test basic group with medium terms."""
327
+ grp = group()
328
+ grp.medium("java", "kotlin")
329
+ self.assertEqual(grp.build(), "java kotlin")
330
+
331
+ def test_group_high_weight(self):
332
+ """Test group with high weight terms."""
333
+ grp = group()
334
+ grp.high("important")
335
+ self.assertEqual(grp.build(), ">important")
336
+
337
+ def test_group_low_weight(self):
338
+ """Test group with low weight terms."""
339
+ grp = group()
340
+ grp.low("minor")
341
+ self.assertEqual(grp.build(), "<minor")
342
+
343
+ def test_mixed_weights_in_group(self):
344
+ """Test mixed weight terms in group."""
345
+ grp = group()
346
+ grp.medium("normal").high("important").low("minor")
347
+ self.assertEqual(grp.build(), "normal >important <minor")
348
+
349
+ def test_group_phrase(self):
350
+ """Test phrase in group."""
351
+ grp = group()
352
+ grp.phrase("deep learning")
353
+ self.assertEqual(grp.build(), '"deep learning"')
354
+
355
+ def test_group_prefix(self):
356
+ """Test prefix in group."""
357
+ grp = group()
358
+ grp.prefix("neural")
359
+ self.assertEqual(grp.build(), "neural*")
360
+
361
+ def test_nested_groups(self):
362
+ """Test nested groups."""
363
+ inner_group = group()
364
+ inner_group.medium("java", "kotlin")
365
+
366
+ outer_group = group()
367
+ outer_group.medium("python").add_group(inner_group)
368
+ self.assertEqual(outer_group.build(), "python (java kotlin)")
369
+
370
+ def test_tilde_group(self):
371
+ """Test tilde group."""
372
+ grp = group()
373
+ grp.medium("old", "outdated")
374
+ grp.is_tilde = True
375
+ self.assertEqual(grp.build(), "old outdated") # Tilde is applied at parent level
376
+
377
+
378
+ class TestFulltextEdgeCases(unittest.TestCase):
379
+ """Test edge cases and error handling - from test_fulltext_search_offline.py"""
380
+
381
+ def test_empty_query_error(self):
382
+ """Test empty query raises error."""
383
+ filter_obj = FulltextFilter(["title", "content"])
384
+ with self.assertRaises(ValueError, msg="Query cannot be empty"):
385
+ filter_obj.compile()
386
+
387
+ def test_no_columns_error(self):
388
+ """Test no columns raises error."""
389
+ filter_obj = FulltextFilter([])
390
+ filter_obj.encourage("test")
391
+ with self.assertRaises(ValueError, msg="Columns must be specified"):
392
+ filter_obj.compile()
393
+
394
+ def test_single_column(self):
395
+ """Test single column search."""
396
+ filter_obj = boolean_match("title").must("python")
397
+ expected = "MATCH(title) AGAINST('+python' IN BOOLEAN MODE)"
398
+ self.assertEqual(filter_obj.compile(), expected)
399
+
400
+ def test_many_columns(self):
401
+ """Test many columns search."""
402
+ filter_obj = boolean_match("title", "content", "tags", "description").must("python")
403
+ expected = "MATCH(title, content, tags, description) AGAINST('+python' IN BOOLEAN MODE)"
404
+ self.assertEqual(filter_obj.compile(), expected)
405
+
406
+ def test_empty_group_building(self):
407
+ """Test empty group building."""
408
+ grp = group()
409
+ result = grp.build()
410
+ self.assertEqual(result, "")
411
+
412
+ def test_unknown_search_mode(self):
413
+ """Test unknown search mode handling."""
414
+ filter_obj = FulltextFilter(["title", "content"], "unknown_mode")
415
+ filter_obj.encourage("test")
416
+
417
+ result = filter_obj.compile()
418
+ # Should default to basic AGAINST syntax
419
+ self.assertEqual(result, "MATCH(title, content) AGAINST('test')")
420
+
421
+
422
+ class TestFulltextLabel(unittest.TestCase):
423
+ """Test FulltextFilter label functionality - from test_fulltext_label.py"""
424
+
425
+ def test_basic_boolean_label(self):
426
+ """Test basic boolean match with label."""
427
+ expr = boolean_match("title", "content").must("python").label("score")
428
+ sql = expr.compile()
429
+
430
+ expected = "MATCH(title, content) AGAINST('+python' IN BOOLEAN MODE) AS score"
431
+ self.assertEqual(sql, expected)
432
+
433
+ def test_basic_natural_label(self):
434
+ """Test basic natural language match with label."""
435
+ expr = natural_match("title", "content", query="machine learning").label("relevance")
436
+ sql = expr.compile()
437
+
438
+ expected = "MATCH(title, content) AGAINST('machine learning') AS relevance"
439
+ self.assertEqual(sql, expected)
440
+
441
+ def test_phrase_search_label(self):
442
+ """Test phrase search with label."""
443
+ expr = boolean_match("title", "content").must('"machine learning"').label("phrase_score")
444
+ sql = expr.compile()
445
+
446
+ expected = 'MATCH(title, content) AGAINST(\'+"machine learning"\' IN BOOLEAN MODE) AS phrase_score'
447
+ self.assertEqual(sql, expected)
448
+
449
+ def test_wildcard_search_label(self):
450
+ """Test wildcard search with label."""
451
+ expr = boolean_match("title", "content").must("python*").label("wildcard_score")
452
+ sql = expr.compile()
453
+
454
+ expected = "MATCH(title, content) AGAINST('+python*' IN BOOLEAN MODE) AS wildcard_score"
455
+ self.assertEqual(sql, expected)
456
+
457
+ def test_multiple_labels(self):
458
+ """Test multiple labels on different expressions."""
459
+ expr1 = boolean_match("title", "content").must("python").label("python_score")
460
+ expr2 = boolean_match("title", "content").must("java").label("java_score")
461
+
462
+ sql1 = expr1.compile()
463
+ sql2 = expr2.compile()
464
+
465
+ expected1 = "MATCH(title, content) AGAINST('+python' IN BOOLEAN MODE) AS python_score"
466
+ expected2 = "MATCH(title, content) AGAINST('+java' IN BOOLEAN MODE) AS java_score"
467
+
468
+ self.assertEqual(sql1, expected1)
469
+ self.assertEqual(sql2, expected2)
470
+
471
+ def test_special_characters_label(self):
472
+ """Test special characters in query with label."""
473
+ expr = boolean_match("title", "content").must("C++").label("cpp_score")
474
+ sql = expr.compile()
475
+
476
+ expected = "MATCH(title, content) AGAINST('+C++' IN BOOLEAN MODE) AS cpp_score"
477
+ self.assertEqual(sql, expected)
478
+
479
+ def test_unicode_label(self):
480
+ """Test unicode characters with label."""
481
+ expr = natural_match("title", "content", query="机器学习").label("ml_score")
482
+ sql = expr.compile()
483
+
484
+ expected = "MATCH(title, content) AGAINST('机器学习') AS ml_score"
485
+ self.assertEqual(sql, expected)
486
+
487
+ def test_numeric_label(self):
488
+ """Test numeric label name."""
489
+ expr = boolean_match("title", "content").must("python").label("score_123")
490
+ sql = expr.compile()
491
+
492
+ expected = "MATCH(title, content) AGAINST('+python' IN BOOLEAN MODE) AS score_123"
493
+ self.assertEqual(sql, expected)
494
+
495
+ def test_underscore_label(self):
496
+ """Test underscore in label name."""
497
+ expr = boolean_match("title", "content").must("python").label("python_relevance_score")
498
+ sql = expr.compile()
499
+
500
+ expected = "MATCH(title, content) AGAINST('+python' IN BOOLEAN MODE) AS python_relevance_score"
501
+ self.assertEqual(sql, expected)
502
+
503
+ def test_long_label(self):
504
+ """Test long label name."""
505
+ long_label = "very_long_label_name_for_fulltext_search_relevance_scoring"
506
+ expr = boolean_match("title", "content").must("python").label(long_label)
507
+ sql = expr.compile()
508
+
509
+ expected = f"MATCH(title, content) AGAINST('+python' IN BOOLEAN MODE) AS {long_label}"
510
+ self.assertEqual(sql, expected)
511
+
512
+ def test_label_with_spaces(self):
513
+ """Test label name with spaces."""
514
+ expr = boolean_match("title", "content").must("python").label("python score")
515
+ sql = expr.compile()
516
+
517
+ expected = "MATCH(title, content) AGAINST('+python' IN BOOLEAN MODE) AS python score"
518
+ self.assertEqual(sql, expected)
519
+
520
+
521
+ class TestFulltextParserSupport(unittest.TestCase):
522
+ """Test Fulltext Parser Support (JSON, NGRAM) - comprehensive SQL generation tests"""
523
+
524
+ def test_fulltext_index_with_json_parser(self):
525
+ """Test FulltextIndex with JSON parser generates correct SQL"""
526
+ from matrixone.sqlalchemy_ext import FulltextParserType
527
+
528
+ index = FulltextIndex("ftidx_json", ["json_data"], parser=FulltextParserType.JSON)
529
+ sql = index._create_index_sql("products")
530
+
531
+ expected_sql = "CREATE FULLTEXT INDEX ftidx_json ON products (json_data) WITH PARSER json"
532
+ self.assertEqual(sql, expected_sql)
533
+
534
+ def test_fulltext_index_with_ngram_parser(self):
535
+ """Test FulltextIndex with NGRAM parser generates correct SQL"""
536
+ from matrixone.sqlalchemy_ext import FulltextParserType
537
+
538
+ index = FulltextIndex("ftidx_ngram", ["title", "content"], parser=FulltextParserType.NGRAM)
539
+ sql = index._create_index_sql("chinese_articles")
540
+
541
+ expected_sql = "CREATE FULLTEXT INDEX ftidx_ngram ON chinese_articles (title, content) WITH PARSER ngram"
542
+ self.assertEqual(sql, expected_sql)
543
+
544
+ def test_fulltext_index_multiple_columns_with_parser(self):
545
+ """Test FulltextIndex with multiple columns and parser"""
546
+ from matrixone.sqlalchemy_ext import FulltextParserType
547
+
548
+ index = FulltextIndex("ftidx_multi_json", ["json1", "json2"], parser=FulltextParserType.JSON)
549
+ sql = index._create_index_sql("src")
550
+
551
+ expected_sql = "CREATE FULLTEXT INDEX ftidx_multi_json ON src (json1, json2) WITH PARSER json"
552
+ self.assertEqual(sql, expected_sql)
553
+
554
+ def test_fulltext_index_without_parser(self):
555
+ """Test FulltextIndex without parser (default behavior)"""
556
+ index = FulltextIndex("ftidx_default", ["title", "content"])
557
+ sql = index._create_index_sql("articles")
558
+
559
+ expected_sql = "CREATE FULLTEXT INDEX ftidx_default ON articles (title, content)"
560
+ self.assertEqual(sql, expected_sql)
561
+ self.assertNotIn("WITH PARSER", sql)
562
+
563
+ def test_fulltext_index_parser_attribute(self):
564
+ """Test that FulltextIndex correctly stores parser attribute"""
565
+ from matrixone.sqlalchemy_ext import FulltextParserType
566
+
567
+ index_json = FulltextIndex("ftidx_json", ["data"], parser=FulltextParserType.JSON)
568
+ self.assertEqual(index_json.parser, FulltextParserType.JSON)
569
+
570
+ index_ngram = FulltextIndex("ftidx_ngram", ["content"], parser=FulltextParserType.NGRAM)
571
+ self.assertEqual(index_ngram.parser, FulltextParserType.NGRAM)
572
+
573
+ index_none = FulltextIndex("ftidx_none", ["content"])
574
+ self.assertIsNone(index_none.parser)
575
+
576
+ def test_fulltext_parser_enum_values(self):
577
+ """Test FulltextParserType enum values"""
578
+ from matrixone.sqlalchemy_ext import FulltextParserType
579
+
580
+ self.assertEqual(FulltextParserType.JSON, "json")
581
+ self.assertEqual(FulltextParserType.NGRAM, "ngram")
582
+
583
+
584
+ class TestBM25AlgorithmSupport(unittest.TestCase):
585
+ """Test BM25 Algorithm Support - comprehensive SQL and configuration tests"""
586
+
587
+ def test_bm25_algorithm_type(self):
588
+ """Test FulltextAlgorithmType.BM25 value"""
589
+ self.assertEqual(FulltextAlgorithmType.BM25, "BM25")
590
+ self.assertEqual(FulltextAlgorithmType.TF_IDF, "TF-IDF")
591
+
592
+ def test_fulltext_index_with_bm25(self):
593
+ """Test FulltextIndex with BM25 algorithm"""
594
+ index = FulltextIndex("ftidx_bm25", ["title", "content"], algorithm=FulltextAlgorithmType.BM25)
595
+
596
+ self.assertEqual(index.algorithm, FulltextAlgorithmType.BM25)
597
+ self.assertEqual(index.name, "ftidx_bm25")
598
+ self.assertEqual(index.get_columns(), ["title", "content"])
599
+
600
+ def test_fulltext_index_with_tfidf(self):
601
+ """Test FulltextIndex with TF-IDF algorithm (default)"""
602
+ index = FulltextIndex("ftidx_tfidf", ["title", "content"], algorithm=FulltextAlgorithmType.TF_IDF)
603
+
604
+ self.assertEqual(index.algorithm, FulltextAlgorithmType.TF_IDF)
605
+
606
+ def test_algorithm_sql_generation(self):
607
+ """Test that algorithm doesn't affect SQL generation (it's a runtime config)"""
608
+ index_bm25 = FulltextIndex("ftidx_test", ["content"], algorithm=FulltextAlgorithmType.BM25)
609
+ index_tfidf = FulltextIndex("ftidx_test", ["content"], algorithm=FulltextAlgorithmType.TF_IDF)
610
+
611
+ sql_bm25 = index_bm25._create_index_sql("articles")
612
+ sql_tfidf = index_tfidf._create_index_sql("articles")
613
+
614
+ # SQL should be same, algorithm is a config setting not part of DDL
615
+ self.assertEqual(sql_bm25, sql_tfidf)
616
+
617
+
618
+ class TestComplexBooleanModeQueries(unittest.TestCase):
619
+ """Test complex boolean mode operators - offline SQL generation tests"""
620
+
621
+ def test_wildcard_suffix_generation(self):
622
+ """Test wildcard suffix (*) in boolean queries - SQL should contain wildcard"""
623
+ # Note: The SDK may not expose wildcard directly in the API
624
+ # This tests raw SQL generation if we were to support it
625
+ expr = boolean_match("title", "content").must("red")
626
+ sql = expr.compile()
627
+
628
+ self.assertIn("+red", sql)
629
+ self.assertIn("IN BOOLEAN MODE", sql)
630
+
631
+ def test_phrase_search_sql_generation(self):
632
+ """Test phrase search (quoted strings) in SQL generation"""
633
+ # Testing the SQL format for phrase search
634
+ # Actual phrase syntax is tested in online tests
635
+ expr = boolean_match("title", "content").must("is not red")
636
+ sql = expr.compile()
637
+
638
+ # Should use + operator for multi-word must
639
+ self.assertIn("+is not red", sql)
640
+ self.assertIn("IN BOOLEAN MODE", sql)
641
+
642
+ def test_complex_boolean_combination(self):
643
+ """Test complex boolean combinations"""
644
+ expr = boolean_match("title", "content").must("database").must_not("mysql").encourage("postgresql")
645
+
646
+ sql = expr.compile()
647
+
648
+ self.assertIn("+database", sql)
649
+ self.assertIn("-mysql", sql)
650
+ self.assertIn("postgresql", sql) # encourage is no prefix
651
+ self.assertIn("IN BOOLEAN MODE", sql)
652
+
653
+ def test_discourage_operator_sql(self):
654
+ """Test discourage operator generates correct SQL"""
655
+ expr = boolean_match("title", "content").must("python").discourage("deprecated")
656
+ sql = expr.compile()
657
+
658
+ self.assertIn("+python", sql)
659
+ self.assertIn("~deprecated", sql)
660
+ self.assertIn("IN BOOLEAN MODE", sql)
661
+
662
+
663
+ class TestNullAndEdgeCases(unittest.TestCase):
664
+ """Test NULL handling and edge cases in SQL generation"""
665
+
666
+ def test_empty_search_string(self):
667
+ """Test that empty search string raises ValueError"""
668
+ with self.assertRaises(ValueError) as context:
669
+ expr = natural_match("title", "content", query="")
670
+ expr.compile()
671
+
672
+ # Verify error message is informative
673
+ self.assertIn("empty", str(context.exception).lower())
674
+
675
+ def test_special_characters_in_search(self):
676
+ """Test special characters are properly handled"""
677
+ expr = boolean_match("title").must("C++")
678
+ sql = expr.compile()
679
+
680
+ self.assertIn("+C++", sql)
681
+
682
+ def test_unicode_in_search_query(self):
683
+ """Test unicode characters in search query"""
684
+ expr = natural_match("title", "content", query="机器学习")
685
+ sql = expr.compile()
686
+
687
+ self.assertIn("机器学习", sql)
688
+ self.assertIn("MATCH(title, content)", sql)
689
+
690
+ def test_very_long_search_query(self):
691
+ """Test very long search queries"""
692
+ long_query = " ".join(["term"] * 100)
693
+ expr = natural_match("content", query=long_query)
694
+ sql = expr.compile()
695
+
696
+ self.assertIn(long_query, sql)
697
+ self.assertIn("MATCH(content)", sql)
698
+
699
+
700
+ class TestIndexCreationVariations(unittest.TestCase):
701
+ """Test various index creation scenarios and SQL generation"""
702
+
703
+ def test_single_column_index(self):
704
+ """Test index on single column"""
705
+ index = FulltextIndex("ftidx_single", "content")
706
+ sql = index._create_index_sql("articles")
707
+
708
+ expected = "CREATE FULLTEXT INDEX ftidx_single ON articles (content)"
709
+ self.assertEqual(sql, expected)
710
+
711
+ def test_three_column_index(self):
712
+ """Test index on three columns"""
713
+ index = FulltextIndex("ftidx_three", ["title", "summary", "content"])
714
+ sql = index._create_index_sql("articles")
715
+
716
+ expected = "CREATE FULLTEXT INDEX ftidx_three ON articles (title, summary, content)"
717
+ self.assertEqual(sql, expected)
718
+
719
+ def test_index_with_all_options(self):
720
+ """Test index with all available options"""
721
+ from matrixone.sqlalchemy_ext import FulltextParserType
722
+
723
+ index = FulltextIndex(
724
+ "ftidx_full", ["json1", "json2"], algorithm=FulltextAlgorithmType.BM25, parser=FulltextParserType.JSON
725
+ )
726
+ sql = index._create_index_sql("src")
727
+
728
+ expected = "CREATE FULLTEXT INDEX ftidx_full ON src (json1, json2) WITH PARSER json"
729
+ self.assertEqual(sql, expected)
730
+ # Algorithm is stored but not in DDL
731
+ self.assertEqual(index.algorithm, FulltextAlgorithmType.BM25)
732
+
733
+ def test_index_name_with_special_chars(self):
734
+ """Test index names with underscores and numbers"""
735
+ index = FulltextIndex("ftidx_test_123", ["content"])
736
+ sql = index._create_index_sql("my_table")
737
+
738
+ self.assertIn("ftidx_test_123", sql)
739
+ self.assertIn("my_table", sql)
740
+
741
+
742
+ class TestSearchBuilderCombinations(unittest.TestCase):
743
+ """Test complex search builder combinations"""
744
+
745
+ def test_must_and_must_not_combination(self):
746
+ """Test combining must and must_not"""
747
+ expr = boolean_match("title", "content").must("python", "tutorial").must_not("advanced", "expert")
748
+ sql = expr.compile()
749
+
750
+ self.assertIn("+python", sql)
751
+ self.assertIn("+tutorial", sql)
752
+ self.assertIn("-advanced", sql)
753
+ self.assertIn("-expert", sql)
754
+
755
+ def test_all_operators_combined(self):
756
+ """Test using all operators together"""
757
+ expr = (
758
+ boolean_match("title", "content")
759
+ .must("python")
760
+ .must_not("deprecated")
761
+ .encourage("tutorial")
762
+ .discourage("advanced")
763
+ )
764
+ sql = expr.compile()
765
+
766
+ self.assertIn("+python", sql)
767
+ self.assertIn("-deprecated", sql)
768
+ self.assertIn("tutorial", sql) # No prefix for encourage
769
+ self.assertIn("~advanced", sql)
770
+
771
+ def test_natural_language_mode_sql(self):
772
+ """Test natural language mode SQL generation"""
773
+ expr = natural_match("title", "content", query="machine learning tutorial")
774
+ sql = expr.compile()
775
+
776
+ self.assertIn("MATCH(title, content)", sql)
777
+ self.assertIn("AGAINST('machine learning tutorial')", sql)
778
+ # Natural language mode can omit the explicit mode clause or include it
779
+ # Depends on implementation - let's just check it's valid
780
+
781
+ def test_multiple_columns_various_orders(self):
782
+ """Test column order preservation in SQL"""
783
+ expr1 = boolean_match("title", "content", "summary").must("test")
784
+ sql1 = expr1.compile()
785
+
786
+ expr2 = boolean_match("summary", "content", "title").must("test")
787
+ sql2 = expr2.compile()
788
+
789
+ # Should preserve the order specified
790
+ self.assertIn("title, content, summary", sql1)
791
+ self.assertIn("summary, content, title", sql2)
792
+
793
+
794
+ if __name__ == '__main__':
795
+ unittest.main()