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,1773 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright 2021 - 2022 Matrix Origin
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ """
18
+ Comprehensive online tests for fulltext functionality.
19
+
20
+ This file consolidates all fulltext-related tests including:
21
+ - Basic fulltext search API functionality
22
+ - Fulltext search in transaction contexts
23
+ - Fulltext index creation and management
24
+ - Async fulltext operations
25
+
26
+ These tests require a running MatrixOne database and test the actual
27
+ fulltext functionality with real database operations.
28
+ """
29
+
30
+ import pytest
31
+ import pytest_asyncio
32
+ from matrixone import Client, AsyncClient, FulltextAlgorithmType, FulltextModeType, FulltextParserType
33
+ from matrixone.sqlalchemy_ext import FulltextIndex, FulltextSearchBuilder
34
+ from matrixone.sqlalchemy_ext import boolean_match, natural_match
35
+ from matrixone.orm import declarative_base
36
+ from sqlalchemy import Column, Integer, String, Text, BigInteger
37
+
38
+
39
+ class TestFulltextComprehensive:
40
+ """Comprehensive test suite for fulltext functionality"""
41
+
42
+ # ============================================================================
43
+ # Basic Fulltext Search API Tests
44
+ # ============================================================================
45
+
46
+ def test_fulltext_search_basic(self, test_client):
47
+ """Test basic fulltext search functionality"""
48
+ # Enable fulltext index functionality using interface
49
+ test_client.fulltext_index.enable_fulltext()
50
+
51
+ # Create test table
52
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_search_test")
53
+ test_client.execute("USE fulltext_search_test")
54
+
55
+ test_client.execute(
56
+ """
57
+ CREATE TABLE IF NOT EXISTS test_documents (
58
+ id INT PRIMARY KEY AUTO_INCREMENT,
59
+ title VARCHAR(200),
60
+ content TEXT
61
+ )
62
+ """
63
+ )
64
+
65
+ # Insert test data
66
+ test_client.execute(
67
+ """
68
+ INSERT INTO test_documents (title, content) VALUES
69
+ ('Machine Learning Guide', 'This is a comprehensive guide to machine learning concepts and algorithms'),
70
+ ('Data Science Handbook', 'A complete handbook covering data science techniques and tools'),
71
+ ('AI Research Paper', 'Latest research in artificial intelligence and neural networks')
72
+ """
73
+ )
74
+
75
+ # Create fulltext index
76
+ test_client.fulltext_index.create("test_documents", name="ftidx_content", columns=["title", "content"])
77
+
78
+ try:
79
+ # Test search functionality
80
+ result = test_client.query(
81
+ "test_documents.title",
82
+ "test_documents.content",
83
+ boolean_match("title", "content").encourage("machine learning"),
84
+ ).execute()
85
+
86
+ assert result is not None
87
+ assert len(result.rows) > 0
88
+
89
+ # Verify that we get results with machine learning content
90
+ found_ml = False
91
+ for row in result.rows:
92
+ if 'machine' in str(row).lower() and 'learning' in str(row).lower():
93
+ found_ml = True
94
+ break
95
+ assert found_ml, "Should find machine learning related content"
96
+
97
+ finally:
98
+ # Clean up
99
+ test_client.fulltext_index.drop("test_documents", "ftidx_content")
100
+ test_client.execute("DROP TABLE test_documents")
101
+ test_client.execute("DROP DATABASE fulltext_search_test")
102
+
103
+ def test_fulltext_search_in_transaction_sync(self, test_client):
104
+ """Test synchronous fulltext search (simplified from transaction)"""
105
+ # Enable fulltext indexing using interface
106
+ test_client.fulltext_index.enable_fulltext()
107
+
108
+ # Create test database and table
109
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_tx_test")
110
+ test_client.execute("USE fulltext_tx_test")
111
+
112
+ test_client.execute(
113
+ """
114
+ CREATE TABLE IF NOT EXISTS test_docs_tx (
115
+ id INT PRIMARY KEY,
116
+ title VARCHAR(100),
117
+ content TEXT
118
+ )
119
+ """
120
+ )
121
+
122
+ # Clear any existing data and insert test data
123
+ test_client.execute("DELETE FROM test_docs_tx")
124
+ test_client.execute(
125
+ """
126
+ INSERT INTO test_docs_tx (id, title, content) VALUES
127
+ (1, 'Python Programming', 'Python is a great programming language for data science'),
128
+ (2, 'Machine Learning', 'Machine learning algorithms can learn from data'),
129
+ (3, 'Database Systems', 'Database systems store and manage data efficiently')
130
+ """
131
+ )
132
+
133
+ # Create fulltext index
134
+ test_client.fulltext_index.create("test_docs_tx", name="ftidx_tx_docs", columns=["title", "content"])
135
+
136
+ try:
137
+ # Test query method
138
+ result = test_client.query(
139
+ "test_docs_tx.title",
140
+ "test_docs_tx.content",
141
+ boolean_match("title", "content").encourage("python programming"),
142
+ ).execute()
143
+
144
+ assert result is not None
145
+ assert len(result.rows) > 0
146
+
147
+ # Verify that we get Python related results
148
+ found_python = False
149
+ for row in result.rows:
150
+ if 'python' in str(row).lower():
151
+ found_python = True
152
+ break
153
+ assert found_python, "Should find Python related content"
154
+
155
+ finally:
156
+ # Clean up
157
+ test_client.fulltext_index.drop("test_docs_tx", "ftidx_tx_docs")
158
+ test_client.execute("DROP TABLE test_docs_tx")
159
+ test_client.execute("DROP DATABASE fulltext_tx_test")
160
+
161
+ @pytest.mark.asyncio
162
+ async def test_fulltext_search_async(self, test_async_client):
163
+ """Test asynchronous fulltext search functionality"""
164
+ # Enable fulltext indexing using interface
165
+ await test_async_client.fulltext_index.enable_fulltext()
166
+
167
+ # Create test table
168
+ await test_async_client.execute("CREATE DATABASE IF NOT EXISTS async_fulltext_test")
169
+ await test_async_client.execute("USE async_fulltext_test")
170
+
171
+ await test_async_client.execute(
172
+ """
173
+ CREATE TABLE IF NOT EXISTS async_documents (
174
+ id INT PRIMARY KEY AUTO_INCREMENT,
175
+ title VARCHAR(200),
176
+ content TEXT
177
+ )
178
+ """
179
+ )
180
+
181
+ # Clear existing data and insert test data
182
+ await test_async_client.execute("DELETE FROM async_documents")
183
+ await test_async_client.execute(
184
+ """
185
+ INSERT INTO async_documents (title, content) VALUES
186
+ ('Database Design', 'Database design principles and best practices'),
187
+ ('SQL Optimization', 'Techniques for optimizing SQL queries and performance'),
188
+ ('NoSQL Systems', 'Understanding NoSQL database systems and their use cases')
189
+ """
190
+ )
191
+
192
+ # Create fulltext index
193
+ await test_async_client.fulltext_index.create(
194
+ "async_documents", name="ftidx_async_content", columns=["title", "content"]
195
+ )
196
+
197
+ try:
198
+ # Test async search functionality
199
+ result = await test_async_client.query(
200
+ "async_documents.title", "async_documents.content", boolean_match("title", "content").encourage("database")
201
+ ).execute()
202
+
203
+ assert result is not None
204
+ assert len(result.rows) > 0
205
+
206
+ # Verify that we get database related results
207
+ found_database = False
208
+ for row in result.rows:
209
+ if 'database' in str(row).lower():
210
+ found_database = True
211
+ break
212
+ assert found_database, "Should find database related content"
213
+
214
+ finally:
215
+ # Clean up
216
+ await test_async_client.fulltext_index.drop("async_documents", "ftidx_async_content")
217
+ await test_async_client.execute("DROP TABLE async_documents")
218
+ await test_async_client.execute("DROP DATABASE async_fulltext_test")
219
+
220
+ @pytest.mark.asyncio
221
+ async def test_fulltext_search_in_transaction_async(self, test_async_client):
222
+ """Test asynchronous fulltext search (simplified from transaction)"""
223
+ # Enable fulltext indexing using interface
224
+ await test_async_client.fulltext_index.enable_fulltext()
225
+
226
+ # Create test database and table
227
+ await test_async_client.execute("CREATE DATABASE IF NOT EXISTS async_fulltext_tx_test")
228
+ await test_async_client.execute("USE async_fulltext_tx_test")
229
+
230
+ await test_async_client.execute(
231
+ """
232
+ CREATE TABLE IF NOT EXISTS async_docs_tx (
233
+ id INT PRIMARY KEY,
234
+ title VARCHAR(100),
235
+ content TEXT
236
+ )
237
+ """
238
+ )
239
+
240
+ # Clear existing data and insert test data
241
+ await test_async_client.execute("DELETE FROM async_docs_tx")
242
+ await test_async_client.execute(
243
+ """
244
+ INSERT INTO async_docs_tx (id, title, content) VALUES
245
+ (1, 'Async Programming', 'Async programming allows concurrent execution'),
246
+ (2, 'Web Development', 'Web development involves frontend and backend'),
247
+ (3, 'Cloud Computing', 'Cloud computing provides scalable resources')
248
+ """
249
+ )
250
+
251
+ # Create fulltext index
252
+ await test_async_client.fulltext_index.create(
253
+ "async_docs_tx", name="ftidx_async_tx_docs", columns=["title", "content"]
254
+ )
255
+
256
+ try:
257
+ # Test query method
258
+ result = await test_async_client.query(
259
+ "async_docs_tx.title",
260
+ "async_docs_tx.content",
261
+ boolean_match("title", "content").encourage("async programming"),
262
+ ).execute()
263
+
264
+ assert result is not None
265
+ assert len(result.rows) > 0
266
+
267
+ # Verify that we get async related results
268
+ found_async = False
269
+ for row in result.rows:
270
+ if 'async' in str(row).lower():
271
+ found_async = True
272
+ break
273
+ assert found_async, "Should find async related content"
274
+
275
+ finally:
276
+ # Clean up
277
+ await test_async_client.fulltext_index.drop("async_docs_tx", "ftidx_async_tx_docs")
278
+ await test_async_client.execute("DROP TABLE async_docs_tx")
279
+ await test_async_client.execute("DROP DATABASE async_fulltext_tx_test")
280
+
281
+ # ============================================================================
282
+ # Fulltext Index Management Tests
283
+ # ============================================================================
284
+
285
+ @pytest.fixture(scope="function")
286
+ def test_table(self, test_client):
287
+ """Create test table for fulltext tests"""
288
+ # Enable fulltext indexing using interface
289
+ test_client.fulltext_index.enable_fulltext()
290
+
291
+ # Create test table
292
+ test_client.execute(
293
+ """
294
+ CREATE TABLE IF NOT EXISTS test_fulltext (
295
+ id BIGINT PRIMARY KEY,
296
+ title VARCHAR(255),
297
+ content TEXT,
298
+ category VARCHAR(50)
299
+ )
300
+ """
301
+ )
302
+
303
+ yield "test_fulltext"
304
+
305
+ # Cleanup
306
+ try:
307
+ test_client.execute("DROP TABLE IF EXISTS test_fulltext")
308
+ except Exception:
309
+ pass
310
+
311
+ def test_create_fulltext_index_sync(self, test_client, test_table):
312
+ """Test creating fulltext index synchronously"""
313
+ # Create fulltext index using client.fulltext_index
314
+ test_client.fulltext_index.create(
315
+ test_table,
316
+ name="ftidx_test",
317
+ columns=["title", "content"],
318
+ algorithm=FulltextAlgorithmType.BM25,
319
+ )
320
+
321
+ # Verify index was created by checking if we can search
322
+ result = test_client.query(
323
+ f"{test_table}.title", f"{test_table}.content", boolean_match("title", "content").encourage("test")
324
+ ).execute()
325
+
326
+ assert result is not None
327
+
328
+ @pytest.mark.asyncio
329
+ async def test_create_fulltext_index_async(self, test_async_client):
330
+ """Test creating fulltext index asynchronously"""
331
+ # Enable fulltext indexing using interface
332
+ await test_async_client.fulltext_index.enable_fulltext()
333
+
334
+ # Create test table
335
+ await test_async_client.execute(
336
+ """
337
+ CREATE TABLE IF NOT EXISTS test_fulltext_async (
338
+ id BIGINT PRIMARY KEY,
339
+ title VARCHAR(255),
340
+ content TEXT
341
+ )
342
+ """
343
+ )
344
+
345
+ try:
346
+ # Create fulltext index
347
+ await test_async_client.fulltext_index.create(
348
+ "test_fulltext_async",
349
+ name="ftidx_async_test",
350
+ columns=["title", "content"],
351
+ algorithm=FulltextAlgorithmType.BM25,
352
+ )
353
+
354
+ # Verify index was created by checking if we can search
355
+ result = await test_async_client.query(
356
+ "test_fulltext_async.title",
357
+ "test_fulltext_async.content",
358
+ boolean_match("title", "content").encourage("test"),
359
+ ).execute()
360
+
361
+ assert result is not None
362
+
363
+ finally:
364
+ # Cleanup
365
+ try:
366
+ await test_async_client.execute("DROP TABLE IF EXISTS test_fulltext_async")
367
+ except Exception:
368
+ pass
369
+
370
+ def test_drop_fulltext_index_sync(self, test_client, test_table):
371
+ """Test dropping fulltext index synchronously"""
372
+ # Create index first
373
+ test_client.fulltext_index.create(test_table, name="ftidx_drop_test", columns=["title", "content"])
374
+
375
+ # Drop the index
376
+ test_client.fulltext_index.drop(test_table, name="ftidx_drop_test")
377
+
378
+ # Verify index was dropped (this should not raise an exception)
379
+ # Note: We can't easily verify the index is gone without checking system tables
380
+ # So we just ensure the drop operation completes successfully
381
+ assert True
382
+
383
+ @pytest.mark.asyncio
384
+ async def test_drop_fulltext_index_async(self, test_async_client):
385
+ """Test dropping fulltext index asynchronously"""
386
+ # Enable fulltext indexing using interface
387
+ await test_async_client.fulltext_index.enable_fulltext()
388
+
389
+ # Create test table
390
+ await test_async_client.execute(
391
+ """
392
+ CREATE TABLE IF NOT EXISTS test_fulltext_drop_async (
393
+ id BIGINT PRIMARY KEY,
394
+ title VARCHAR(255),
395
+ content TEXT
396
+ )
397
+ """
398
+ )
399
+
400
+ try:
401
+ # Create index first
402
+ await test_async_client.fulltext_index.create(
403
+ "test_fulltext_drop_async",
404
+ name="ftidx_drop_async_test",
405
+ columns=["title", "content"],
406
+ )
407
+
408
+ # Drop the index
409
+ await test_async_client.fulltext_index.drop("test_fulltext_drop_async", name="ftidx_drop_async_test")
410
+
411
+ # Verify index was dropped
412
+ assert True
413
+
414
+ finally:
415
+ # Cleanup
416
+ try:
417
+ await test_async_client.execute("DROP TABLE IF EXISTS test_fulltext_drop_async")
418
+ except Exception:
419
+ pass
420
+
421
+ # ============================================================================
422
+ # Transaction Context Tests
423
+ # ============================================================================
424
+
425
+ def test_fulltext_search_with_manual_transaction(self, test_client):
426
+ """Test fulltext search using simple_query interface"""
427
+ # Enable fulltext indexing using interface
428
+ test_client.fulltext_index.enable_fulltext()
429
+
430
+ # Create test database and table
431
+ test_client.execute("CREATE DATABASE IF NOT EXISTS manual_fulltext_tx_test")
432
+ test_client.execute("USE manual_fulltext_tx_test")
433
+
434
+ test_client.execute(
435
+ """
436
+ CREATE TABLE IF NOT EXISTS manual_tx_docs (
437
+ id INT PRIMARY KEY,
438
+ title VARCHAR(100),
439
+ content TEXT
440
+ )
441
+ """
442
+ )
443
+
444
+ # Clear any existing data and insert test data
445
+ test_client.execute("DELETE FROM manual_tx_docs")
446
+ test_client.execute(
447
+ """
448
+ INSERT INTO manual_tx_docs (id, title, content) VALUES
449
+ (1, 'Transaction Management', 'Transactions ensure data consistency'),
450
+ (2, 'ACID Properties', 'ACID properties guarantee reliable processing'),
451
+ (3, 'Concurrency Control', 'Concurrency control manages simultaneous access')
452
+ """
453
+ )
454
+
455
+ # Create fulltext index
456
+ test_client.fulltext_index.create("manual_tx_docs", name="ftidx_manual_tx", columns=["title", "content"])
457
+
458
+ try:
459
+ # Test using query in context
460
+ result = test_client.query(
461
+ "manual_tx_docs.title",
462
+ "manual_tx_docs.content",
463
+ boolean_match("title", "content").encourage("transaction management"),
464
+ ).execute()
465
+
466
+ assert result is not None
467
+ assert len(result.rows) > 0
468
+
469
+ # Verify that we get transaction related results
470
+ found_transaction = False
471
+ for row in result.rows:
472
+ if 'transaction' in str(row).lower():
473
+ found_transaction = True
474
+ break
475
+ assert found_transaction, "Should find transaction related content"
476
+
477
+ finally:
478
+ # Clean up
479
+ test_client.fulltext_index.drop("manual_tx_docs", "ftidx_manual_tx")
480
+ test_client.execute("DROP TABLE manual_tx_docs")
481
+ test_client.execute("DROP DATABASE manual_fulltext_tx_test")
482
+
483
+ @pytest.mark.asyncio
484
+ async def test_fulltext_search_with_manual_async_transaction(self, test_async_client):
485
+ """Test async fulltext search using simple_query interface"""
486
+ # Enable fulltext indexing using interface
487
+ await test_async_client.fulltext_index.enable_fulltext()
488
+
489
+ # Create test database and table
490
+ await test_async_client.execute("CREATE DATABASE IF NOT EXISTS manual_async_fulltext_tx_test")
491
+ await test_async_client.execute("USE manual_async_fulltext_tx_test")
492
+
493
+ await test_async_client.execute(
494
+ """
495
+ CREATE TABLE IF NOT EXISTS manual_async_tx_docs (
496
+ id INT PRIMARY KEY,
497
+ title VARCHAR(100),
498
+ content TEXT
499
+ )
500
+ """
501
+ )
502
+
503
+ # Clear existing data and insert test data
504
+ await test_async_client.execute("DELETE FROM manual_async_tx_docs")
505
+ await test_async_client.execute(
506
+ """
507
+ INSERT INTO manual_async_tx_docs (id, title, content) VALUES
508
+ (1, 'Async Transactions', 'Async transactions handle concurrent operations'),
509
+ (2, 'Event Loop', 'Event loop manages async operations efficiently'),
510
+ (3, 'Promise Handling', 'Promise handling manages async results')
511
+ """
512
+ )
513
+
514
+ # Create fulltext index
515
+ await test_async_client.fulltext_index.create(
516
+ "manual_async_tx_docs",
517
+ name="ftidx_manual_async_tx",
518
+ columns=["title", "content"],
519
+ )
520
+
521
+ try:
522
+ # Test using query in async context
523
+ result = await test_async_client.query(
524
+ "manual_async_tx_docs.title",
525
+ "manual_async_tx_docs.content",
526
+ boolean_match("title", "content").encourage("async transactions"),
527
+ ).execute()
528
+
529
+ assert result is not None
530
+ assert len(result.rows) > 0
531
+
532
+ # Verify that we get async transaction related results
533
+ found_async_tx = False
534
+ for row in result.rows:
535
+ if 'async' in str(row).lower() and 'transaction' in str(row).lower():
536
+ found_async_tx = True
537
+ break
538
+ assert found_async_tx, "Should find async transaction related content"
539
+
540
+ finally:
541
+ # Clean up
542
+ await test_async_client.fulltext_index.drop("manual_async_tx_docs", "ftidx_manual_async_tx")
543
+ await test_async_client.execute("DROP TABLE manual_async_tx_docs")
544
+ await test_async_client.execute("DROP DATABASE manual_async_fulltext_tx_test")
545
+
546
+ @pytest.mark.asyncio
547
+ async def test_async_simple_query_advanced_features(self, test_async_client):
548
+ """Test advanced async simple_query features"""
549
+ # Create test database and table
550
+ await test_async_client.execute("CREATE DATABASE IF NOT EXISTS async_advanced_test")
551
+ await test_async_client.execute("USE async_advanced_test")
552
+
553
+ await test_async_client.execute("DROP TABLE IF EXISTS async_advanced_docs")
554
+ await test_async_client.execute(
555
+ """
556
+ CREATE TABLE async_advanced_docs (
557
+ id INT AUTO_INCREMENT PRIMARY KEY,
558
+ title VARCHAR(255) NOT NULL,
559
+ content TEXT NOT NULL,
560
+ category VARCHAR(100) NOT NULL,
561
+ priority INT DEFAULT 1,
562
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
563
+ )
564
+ """
565
+ )
566
+
567
+ # Insert test data
568
+ test_docs = [
569
+ (
570
+ "High Priority Python Guide",
571
+ "Comprehensive Python programming guide with async features",
572
+ "Programming",
573
+ 3,
574
+ ),
575
+ (
576
+ "Medium Priority JavaScript Tutorial",
577
+ "JavaScript tutorial covering async/await patterns",
578
+ "Programming",
579
+ 2,
580
+ ),
581
+ (
582
+ "Low Priority Database Basics",
583
+ "Basic database concepts and transaction handling",
584
+ "Database",
585
+ 1,
586
+ ),
587
+ (
588
+ "High Priority Async Patterns",
589
+ "Advanced async programming patterns and best practices",
590
+ "Programming",
591
+ 3,
592
+ ),
593
+ (
594
+ "Medium Priority Web Development",
595
+ "Modern web development with async JavaScript",
596
+ "Web",
597
+ 2,
598
+ ),
599
+ ]
600
+
601
+ for title, content, category, priority in test_docs:
602
+ await test_async_client.execute(
603
+ f"""
604
+ INSERT INTO async_advanced_docs (title, content, category, priority)
605
+ VALUES ('{title}', '{content}', '{category}', {priority})
606
+ """
607
+ )
608
+
609
+ try:
610
+ # Create fulltext index
611
+ await test_async_client.fulltext_index.create("async_advanced_docs", "ftidx_advanced", ["title", "content"])
612
+
613
+ # Test 1: Complex boolean search with multiple conditions
614
+ result = (
615
+ await test_async_client.query(
616
+ "async_advanced_docs.title",
617
+ "async_advanced_docs.content",
618
+ boolean_match("title", "content").must("async").must_not("basic").label("score"),
619
+ )
620
+ .filter("async_advanced_docs.priority >= 2")
621
+ .order_by("score DESC")
622
+ .limit(3)
623
+ .execute()
624
+ )
625
+
626
+ assert result is not None
627
+ assert len(result.rows) > 0
628
+
629
+ # Verify all results contain "async" in title or content (priority check is done in WHERE clause)
630
+ for row in result.rows:
631
+ title_content = f"{row[1]} {row[2]}".lower() # title and content
632
+ assert "async" in title_content
633
+ assert "basic" not in title_content
634
+
635
+ # Test 2: Search with custom score alias and ordering
636
+ result = (
637
+ await test_async_client.query(
638
+ "async_advanced_docs.title",
639
+ "async_advanced_docs.content",
640
+ boolean_match("title", "content").encourage("programming").label("relevance_score"),
641
+ )
642
+ .order_by("async_advanced_docs.priority DESC")
643
+ .execute()
644
+ )
645
+
646
+ assert result is not None
647
+ assert len(result.rows) > 0
648
+
649
+ # Test 3: Multiple WHERE conditions
650
+ result = (
651
+ await test_async_client.query("async_advanced_docs.title", "async_advanced_docs.content")
652
+ .filter(
653
+ boolean_match("title", "content").encourage("programming"),
654
+ "async_advanced_docs.category = 'Programming'",
655
+ "async_advanced_docs.priority > 1",
656
+ )
657
+ .execute()
658
+ )
659
+
660
+ assert result is not None
661
+ # All results should be Programming category with priority > 1 (category check is done in WHERE clause)
662
+ # Since we only select title and content, we can't directly check category in results
663
+ assert len(result.rows) > 0 # Should have results matching the conditions
664
+
665
+ # Test 4: Basic query functionality (replacing explain functionality)
666
+ result = (
667
+ await test_async_client.query(
668
+ "async_advanced_docs.title",
669
+ "async_advanced_docs.content",
670
+ boolean_match("title", "content").encourage("javascript").label("score"),
671
+ )
672
+ .filter("async_advanced_docs.priority >= 2")
673
+ .execute()
674
+ )
675
+
676
+ assert result is not None
677
+ # Should find documents with "javascript" in title or content and priority >= 2
678
+ # From test data: "Medium Priority JavaScript Tutorial" has priority 2 and contains "javascript"
679
+ assert len(result.rows) > 0, "Should find documents containing 'javascript' with priority >= 2"
680
+
681
+ # Verify score column is present and has numeric values
682
+ assert "score" in result.columns
683
+ score_column_index = result.columns.index("score")
684
+ for row in result.rows:
685
+ score = row[score_column_index]
686
+ assert isinstance(score, (int, float)), f"Score should be numeric, got {type(score)}"
687
+ assert score >= 0, f"Score should be non-negative, got {score}"
688
+
689
+ # Test 5: Method chaining verification
690
+ result = (
691
+ await test_async_client.query(
692
+ "async_advanced_docs.title",
693
+ "async_advanced_docs.content",
694
+ boolean_match("title", "content").encourage("python").label("score"),
695
+ )
696
+ .filter("async_advanced_docs.category = 'Programming'")
697
+ .order_by("score DESC")
698
+ .limit(5)
699
+ .execute()
700
+ )
701
+
702
+ assert result is not None
703
+ # Should find Programming documents containing "python"
704
+ # From test data: "High Priority Python Guide" is Programming category and contains "python"
705
+ assert len(result.rows) > 0, "Should find Programming documents containing 'python'"
706
+
707
+ # Verify score column is present and has numeric values
708
+ assert "score" in result.columns
709
+ score_column_index = result.columns.index("score")
710
+ for row in result.rows:
711
+ score = row[score_column_index]
712
+ assert isinstance(score, (int, float)), f"Score should be numeric, got {type(score)}"
713
+ assert score >= 0, f"Score should be non-negative, got {score}"
714
+
715
+ finally:
716
+ # Clean up
717
+ await test_async_client.fulltext_index.drop("async_advanced_docs", "ftidx_advanced")
718
+ await test_async_client.execute("DROP TABLE async_advanced_docs")
719
+ await test_async_client.execute("DROP DATABASE async_advanced_test")
720
+
721
+ @pytest.mark.asyncio
722
+ async def test_async_simple_query_concurrent_operations(self, test_async_client):
723
+ """Test concurrent async simple_query operations"""
724
+ import asyncio
725
+
726
+ # Create test database and table
727
+ await test_async_client.execute("CREATE DATABASE IF NOT EXISTS async_concurrent_test")
728
+ await test_async_client.execute("USE async_concurrent_test")
729
+
730
+ await test_async_client.execute("DROP TABLE IF EXISTS async_concurrent_docs")
731
+ await test_async_client.execute(
732
+ """
733
+ CREATE TABLE async_concurrent_docs (
734
+ id INT AUTO_INCREMENT PRIMARY KEY,
735
+ title VARCHAR(255) NOT NULL,
736
+ content TEXT NOT NULL,
737
+ category VARCHAR(100) NOT NULL
738
+ )
739
+ """
740
+ )
741
+
742
+ # Insert test data
743
+ test_docs = [
744
+ (
745
+ "Python Async Programming",
746
+ "Learn Python async programming with asyncio",
747
+ "Programming",
748
+ ),
749
+ (
750
+ "JavaScript Async Patterns",
751
+ "Modern JavaScript async patterns and promises",
752
+ "Programming",
753
+ ),
754
+ (
755
+ "Database Async Operations",
756
+ "Async database operations and connection pooling",
757
+ "Database",
758
+ ),
759
+ ("Web Async Development", "Async web development with modern frameworks", "Web"),
760
+ (
761
+ "Async Testing Strategies",
762
+ "Testing async code and handling async test cases",
763
+ "Testing",
764
+ ),
765
+ ]
766
+
767
+ for title, content, category in test_docs:
768
+ await test_async_client.execute(
769
+ f"""
770
+ INSERT INTO async_concurrent_docs (title, content, category)
771
+ VALUES ('{title}', '{content}', '{category}')
772
+ """
773
+ )
774
+
775
+ try:
776
+ # Create fulltext index
777
+ await test_async_client.fulltext_index.create("async_concurrent_docs", "ftidx_concurrent", ["title", "content"])
778
+
779
+ # Create multiple concurrent search tasks (use database prefix to avoid connection pool issues)
780
+ table_name = "async_concurrent_test.async_concurrent_docs"
781
+ tasks = [
782
+ test_async_client.query(
783
+ f"{table_name}.title", f"{table_name}.content", boolean_match("title", "content").encourage("python")
784
+ ).execute(),
785
+ test_async_client.query(
786
+ f"{table_name}.title", f"{table_name}.content", boolean_match("title", "content").encourage("javascript")
787
+ ).execute(),
788
+ test_async_client.query(
789
+ f"{table_name}.title", f"{table_name}.content", boolean_match("title", "content").encourage("database")
790
+ ).execute(),
791
+ test_async_client.query(
792
+ f"{table_name}.title", f"{table_name}.content", boolean_match("title", "content").encourage("web")
793
+ ).execute(),
794
+ test_async_client.query(
795
+ f"{table_name}.title", f"{table_name}.content", boolean_match("title", "content").encourage("testing")
796
+ ).execute(),
797
+ ]
798
+
799
+ # Execute all searches concurrently
800
+ results = await asyncio.gather(*tasks)
801
+
802
+ # All searches should complete successfully
803
+ assert len(results) == 5
804
+ for result in results:
805
+ assert result is not None
806
+ assert isinstance(result.rows, list)
807
+ assert len(result.rows) > 0
808
+
809
+ # Verify each search returned relevant results (row[0] is title, row[1] is content)
810
+ python_results = results[0]
811
+ assert any("python" in str(row[0]).lower() or "python" in str(row[1]).lower() for row in python_results.rows)
812
+
813
+ javascript_results = results[1]
814
+ assert any(
815
+ "javascript" in str(row[0]).lower() or "javascript" in str(row[1]).lower() for row in javascript_results.rows
816
+ )
817
+
818
+ database_results = results[2]
819
+ assert any(
820
+ "database" in str(row[0]).lower() or "database" in str(row[1]).lower() for row in database_results.rows
821
+ )
822
+
823
+ finally:
824
+ # Clean up
825
+ try:
826
+ await test_async_client.fulltext_index.drop(
827
+ "async_concurrent_test.async_concurrent_docs", "ftidx_concurrent"
828
+ )
829
+ await test_async_client.execute("DROP TABLE async_concurrent_test.async_concurrent_docs")
830
+ await test_async_client.execute("DROP DATABASE async_concurrent_test")
831
+ except Exception as e:
832
+ print(f"Cleanup warning: {e}")
833
+
834
+ # ============================================================================
835
+ # Advanced Fulltext Features Tests
836
+ # ============================================================================
837
+
838
+ def test_fulltext_search_with_different_modes(self, test_client):
839
+ """Test fulltext search with different search modes"""
840
+ # Enable fulltext indexing using interface
841
+ test_client.fulltext_index.enable_fulltext()
842
+
843
+ # Create test database and table
844
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_modes_test")
845
+ test_client.execute("USE fulltext_modes_test")
846
+
847
+ test_client.execute(
848
+ """
849
+ CREATE TABLE IF NOT EXISTS test_modes (
850
+ id INT PRIMARY KEY,
851
+ title VARCHAR(100),
852
+ content TEXT
853
+ )
854
+ """
855
+ )
856
+
857
+ # Clear any existing data and insert test data
858
+ test_client.execute("DELETE FROM test_modes")
859
+ test_client.execute(
860
+ """
861
+ INSERT INTO test_modes (id, title, content) VALUES
862
+ (1, 'Natural Language Processing', 'NLP is a field of AI that focuses on language'),
863
+ (2, 'Machine Learning Algorithms', 'ML algorithms learn patterns from data'),
864
+ (3, 'Deep Learning Networks', 'Deep learning uses neural networks with multiple layers')
865
+ """
866
+ )
867
+
868
+ # Create fulltext index
869
+ test_client.fulltext_index.create("test_modes", name="ftidx_modes", columns=["title", "content"])
870
+
871
+ try:
872
+ # Test natural language mode
873
+ result = test_client.query(
874
+ "test_modes.title", "test_modes.content", boolean_match("title", "content").encourage("machine learning")
875
+ ).execute()
876
+ assert result is not None
877
+
878
+ # Test boolean mode using must_have
879
+ result = test_client.query(
880
+ "test_modes.title", "test_modes.content", boolean_match("title", "content").must("machine", "learning")
881
+ ).execute()
882
+ assert result is not None
883
+
884
+ finally:
885
+ # Clean up
886
+ test_client.fulltext_index.drop("test_modes", "ftidx_modes")
887
+ test_client.execute("DROP TABLE test_modes")
888
+ test_client.execute("DROP DATABASE fulltext_modes_test")
889
+
890
+ def test_fulltext_search_multiple_columns(self, test_client):
891
+ """Test fulltext search across multiple columns"""
892
+ # Enable fulltext indexing using interface
893
+ test_client.fulltext_index.enable_fulltext()
894
+
895
+ # Create test database and table
896
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_multi_col_test")
897
+ test_client.execute("USE fulltext_multi_col_test")
898
+
899
+ test_client.execute(
900
+ """
901
+ CREATE TABLE IF NOT EXISTS test_multi_col (
902
+ id INT PRIMARY KEY,
903
+ title VARCHAR(100),
904
+ content TEXT,
905
+ tags VARCHAR(200)
906
+ )
907
+ """
908
+ )
909
+
910
+ # Clear any existing data and insert test data
911
+ test_client.execute("DELETE FROM test_multi_col")
912
+ test_client.execute(
913
+ """
914
+ INSERT INTO test_multi_col (id, title, content, tags) VALUES
915
+ (1, 'Python Programming', 'Python is great for data science', 'python, programming, data'),
916
+ (2, 'JavaScript Development', 'JavaScript powers web applications', 'javascript, web, frontend'),
917
+ (3, 'Database Design', 'Good database design is crucial', 'database, design, sql')
918
+ """
919
+ )
920
+
921
+ # Create fulltext index on multiple columns
922
+ test_client.fulltext_index.create(
923
+ "test_multi_col",
924
+ name="ftidx_multi_col",
925
+ columns=["title", "content", "tags"],
926
+ )
927
+
928
+ try:
929
+ # Test search across multiple columns
930
+ result = test_client.query(
931
+ "test_multi_col.title",
932
+ "test_multi_col.content",
933
+ "test_multi_col.tags",
934
+ boolean_match("title", "content", "tags").encourage("python"),
935
+ ).execute()
936
+
937
+ assert result is not None
938
+ # Note: Fulltext search might not return results if no exact matches
939
+ # Just verify the search executes without error
940
+ if len(result.rows) > 0:
941
+ # Verify that we get Python related results
942
+ found_python = False
943
+ for row in result.rows:
944
+ if 'python' in str(row).lower():
945
+ found_python = True
946
+ break
947
+ assert found_python, "Should find Python related content"
948
+
949
+ finally:
950
+ # Clean up
951
+ test_client.fulltext_index.drop("test_multi_col", "ftidx_multi_col")
952
+ test_client.execute("DROP TABLE test_multi_col")
953
+ test_client.execute("DROP DATABASE fulltext_multi_col_test")
954
+
955
+ # ============================================================================
956
+ # JSON Parser Tests
957
+ # ============================================================================
958
+
959
+ def test_fulltext_json_parser_basic(self, test_client):
960
+ """Test JSON parser for fulltext index - basic functionality"""
961
+ from matrixone.orm import declarative_base
962
+ from sqlalchemy import BigInteger, Column, Text
963
+
964
+ # Enable fulltext indexing
965
+ test_client.fulltext_index.enable_fulltext()
966
+
967
+ # Create test database
968
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_json_test")
969
+ test_client.execute("USE fulltext_json_test")
970
+
971
+ # Define ORM model with JSON parser
972
+ Base = declarative_base()
973
+
974
+ class JsonDoc(Base):
975
+ __tablename__ = "json_docs"
976
+ id = Column(BigInteger, primary_key=True)
977
+ json_content = Column(Text)
978
+
979
+ __table_args__ = (FulltextIndex("ftidx_json", "json_content", parser=FulltextParserType.JSON),)
980
+
981
+ # Create table using ORM
982
+ try:
983
+ test_client.create_table(JsonDoc)
984
+
985
+ # Verify index was created with JSON parser
986
+ result = test_client.execute("SHOW CREATE TABLE json_docs")
987
+ create_stmt = result.fetchall()[0][1]
988
+ assert "WITH PARSER json" in create_stmt, "Index should have WITH PARSER json clause"
989
+ assert "FULLTEXT" in create_stmt, "Index should be FULLTEXT type"
990
+
991
+ # Insert test data using client interface
992
+ test_data = [
993
+ {"id": 1, "json_content": '{"title": "Python Tutorial", "tags": ["python", "programming"]}'},
994
+ {"id": 2, "json_content": '{"title": "Machine Learning", "tags": ["AI", "data science"]}'},
995
+ {"id": 3, "json_content": '{"title": "Database Design", "tags": ["SQL", "database"]}'},
996
+ ]
997
+ test_client.batch_insert(JsonDoc, test_data)
998
+
999
+ # Test search functionality
1000
+ result = test_client.query(JsonDoc).filter(boolean_match(JsonDoc.json_content).must("python")).execute()
1001
+
1002
+ assert result is not None
1003
+ rows = result.fetchall()
1004
+ assert len(rows) >= 1, "Should find at least one result with 'python'"
1005
+
1006
+ # Verify the result contains the expected JSON document
1007
+ found = False
1008
+ for row in rows:
1009
+ if 'python' in str(row.json_content).lower():
1010
+ found = True
1011
+ break
1012
+ assert found, "Should find JSON document containing 'python'"
1013
+
1014
+ finally:
1015
+ # Clean up
1016
+ test_client.drop_table(JsonDoc)
1017
+ test_client.execute("DROP DATABASE fulltext_json_test")
1018
+
1019
+ def test_fulltext_json_parser_multiple_columns(self, test_client):
1020
+ """Test JSON parser with multiple columns"""
1021
+ from matrixone.orm import declarative_base
1022
+ from sqlalchemy import BigInteger, Column, String, Text
1023
+
1024
+ # Enable fulltext indexing
1025
+ test_client.fulltext_index.enable_fulltext()
1026
+
1027
+ # Create test database
1028
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_json_multi_test")
1029
+ test_client.execute("USE fulltext_json_multi_test")
1030
+
1031
+ # Define ORM model with JSON parser on multiple columns
1032
+ Base = declarative_base()
1033
+
1034
+ class JsonMulti(Base):
1035
+ __tablename__ = "json_multi"
1036
+ id = Column(BigInteger, primary_key=True)
1037
+ json1 = Column(Text)
1038
+ json2 = Column(String(1000))
1039
+
1040
+ __table_args__ = (FulltextIndex("ftidx_json_multi", ["json1", "json2"], parser=FulltextParserType.JSON),)
1041
+
1042
+ # Create table using ORM
1043
+ try:
1044
+ test_client.create_table(JsonMulti)
1045
+
1046
+ # Verify index was created with JSON parser
1047
+ result = test_client.execute("SHOW CREATE TABLE json_multi")
1048
+ create_stmt = result.fetchall()[0][1]
1049
+ assert "WITH PARSER json" in create_stmt, "Index should have WITH PARSER json clause"
1050
+ assert "ftidx_json_multi" in create_stmt, "Index name should be present"
1051
+
1052
+ # Insert test data using client interface
1053
+ test_data = [
1054
+ {"id": 1, "json1": '{"name": "red apple"}', "json2": '{"color": "red", "taste": "sweet"}'},
1055
+ {"id": 2, "json1": '{"name": "blue sky"}', "json2": '{"weather": "sunny", "season": "summer"}'},
1056
+ {"id": 3, "json1": '{"name": "green tree"}', "json2": '{"type": "oak", "color": "green"}'},
1057
+ ]
1058
+ test_client.batch_insert(JsonMulti, test_data)
1059
+
1060
+ # Test search on multiple columns
1061
+ result = (
1062
+ test_client.query(JsonMulti).filter(boolean_match(JsonMulti.json1, JsonMulti.json2).must("red")).execute()
1063
+ )
1064
+
1065
+ assert result is not None
1066
+ rows = result.fetchall()
1067
+ assert len(rows) >= 1, "Should find results with 'red'"
1068
+
1069
+ # Verify result
1070
+ found_red = False
1071
+ for row in rows:
1072
+ if row.id == 1: # ID should be 1
1073
+ found_red = True
1074
+ break
1075
+ assert found_red, "Should find the red apple document"
1076
+
1077
+ finally:
1078
+ # Clean up
1079
+ test_client.drop_table(JsonMulti)
1080
+ test_client.execute("DROP DATABASE fulltext_json_multi_test")
1081
+
1082
+ def test_fulltext_json_parser_chinese_content(self, test_client):
1083
+ """Test JSON parser with Chinese content"""
1084
+ from matrixone.orm import declarative_base
1085
+ from sqlalchemy import BigInteger, Column, Text
1086
+
1087
+ # Enable fulltext indexing
1088
+ test_client.fulltext_index.enable_fulltext()
1089
+
1090
+ # Create test database
1091
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_json_chinese_test")
1092
+ test_client.execute("USE fulltext_json_chinese_test")
1093
+
1094
+ # Define ORM model with JSON parser
1095
+ Base = declarative_base()
1096
+
1097
+ class JsonChinese(Base):
1098
+ __tablename__ = "json_chinese"
1099
+ id = Column(BigInteger, primary_key=True)
1100
+ json_data = Column(Text)
1101
+
1102
+ __table_args__ = (FulltextIndex("ftidx_json_chinese", "json_data", parser=FulltextParserType.JSON),)
1103
+
1104
+ # Create table using ORM
1105
+ try:
1106
+ test_client.create_table(JsonChinese)
1107
+
1108
+ # Insert Chinese JSON data using client interface
1109
+ test_data = [
1110
+ {"id": 1, "json_data": '{"title": "中文學習教材", "description": "適合初學者"}'},
1111
+ {"id": 2, "json_data": '{"title": "兒童中文", "description": "遠東兒童中文"}'},
1112
+ {"id": 3, "json_data": '{"title": "English Book", "description": "For beginners"}'},
1113
+ ]
1114
+ test_client.batch_insert(JsonChinese, test_data)
1115
+
1116
+ # Test search for Chinese content
1117
+ result = (
1118
+ test_client.query(JsonChinese).filter(boolean_match(JsonChinese.json_data).must("中文學習教材")).execute()
1119
+ )
1120
+
1121
+ assert result is not None
1122
+ rows = result.fetchall()
1123
+ assert len(rows) >= 1, "Should find Chinese content"
1124
+
1125
+ # Verify result contains the expected Chinese document
1126
+ found_chinese = False
1127
+ for row in rows:
1128
+ if row.id == 1: # ID should be 1
1129
+ found_chinese = True
1130
+ assert "中文學習教材" in str(row.json_data), "Should contain the Chinese text"
1131
+ break
1132
+ assert found_chinese, "Should find the Chinese learning material document"
1133
+
1134
+ finally:
1135
+ # Clean up
1136
+ test_client.drop_table(JsonChinese)
1137
+ test_client.execute("DROP DATABASE fulltext_json_chinese_test")
1138
+
1139
+ def test_fulltext_json_parser_orm_integration(self, test_client):
1140
+ """Test JSON parser with ORM integration"""
1141
+ from matrixone.orm import declarative_base
1142
+ from sqlalchemy import BigInteger, Column, Text
1143
+
1144
+ # Enable fulltext indexing
1145
+ test_client.fulltext_index.enable_fulltext()
1146
+
1147
+ # Create test database
1148
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_json_orm_test")
1149
+ test_client.execute("USE fulltext_json_orm_test")
1150
+
1151
+ # Define ORM model with JSON parser index
1152
+ Base = declarative_base()
1153
+
1154
+ class JsonDocument(Base):
1155
+ __tablename__ = "json_documents"
1156
+ id = Column(BigInteger, primary_key=True)
1157
+ json_content = Column(Text)
1158
+
1159
+ __table_args__ = (FulltextIndex("idx_json_content", "json_content", parser=FulltextParserType.JSON),)
1160
+
1161
+ try:
1162
+ # Create table using ORM
1163
+ test_client.create_table(JsonDocument)
1164
+
1165
+ # Verify index was created correctly
1166
+ result = test_client.execute("SHOW CREATE TABLE json_documents")
1167
+ create_stmt = result.fetchall()[0][1]
1168
+ assert "WITH PARSER json" in create_stmt, "ORM-created index should have WITH PARSER json"
1169
+ assert "FULLTEXT" in create_stmt, "Index should be FULLTEXT type"
1170
+ assert "idx_json_content" in create_stmt, "Index name should be present"
1171
+
1172
+ # Insert test data using client interface
1173
+ test_data = [
1174
+ {"id": 1, "json_content": '{"framework": "Django", "language": "Python"}'},
1175
+ {"id": 2, "json_content": '{"framework": "Flask", "language": "Python"}'},
1176
+ {"id": 3, "json_content": '{"framework": "Express", "language": "JavaScript"}'},
1177
+ ]
1178
+ test_client.batch_insert(JsonDocument, test_data)
1179
+
1180
+ # Test ORM query with JSON parser
1181
+ result = (
1182
+ test_client.query(JsonDocument).filter(boolean_match(JsonDocument.json_content).must("Django")).execute()
1183
+ )
1184
+
1185
+ assert result is not None
1186
+ rows = result.fetchall()
1187
+ assert len(rows) >= 1, "Should find Django document"
1188
+
1189
+ # Verify the result
1190
+ found_django = False
1191
+ for row in rows:
1192
+ if row.id == 1:
1193
+ found_django = True
1194
+ assert "Django" in row.json_content
1195
+ break
1196
+ assert found_django, "Should find the Django framework document"
1197
+
1198
+ finally:
1199
+ # Clean up
1200
+ test_client.drop_table(JsonDocument)
1201
+ test_client.execute("DROP DATABASE fulltext_json_orm_test")
1202
+
1203
+ def test_fulltext_json_parser_colon_handling(self, test_client):
1204
+ """
1205
+ Test that JSON strings with colons are properly handled in batch_insert.
1206
+
1207
+ This is a regression test for the issue where SQLAlchemy's text() function
1208
+ would interpret colons in JSON strings (like {"a":1}) as bind parameters (:1),
1209
+ causing "A value is required for bind parameter" errors.
1210
+
1211
+ The fix uses exec_driver_sql() to bypass SQLAlchemy's parameter parsing.
1212
+ """
1213
+ from matrixone.orm import declarative_base
1214
+ from sqlalchemy import BigInteger, Column, Text
1215
+
1216
+ # Enable fulltext indexing
1217
+ test_client.fulltext_index.enable_fulltext()
1218
+
1219
+ # Create test database
1220
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_json_colon_test")
1221
+ test_client.execute("USE fulltext_json_colon_test")
1222
+
1223
+ # Define ORM model
1224
+ Base = declarative_base()
1225
+
1226
+ class JsonColonTest(Base):
1227
+ __tablename__ = "json_colon_test"
1228
+ id = Column(BigInteger, primary_key=True)
1229
+ json_data = Column(Text)
1230
+
1231
+ __table_args__ = (FulltextIndex("idx_json_data", "json_data", parser=FulltextParserType.JSON),)
1232
+
1233
+ try:
1234
+ # Create table
1235
+ test_client.create_table(JsonColonTest)
1236
+
1237
+ # Critical test: Insert JSON data with colons
1238
+ # This would fail with "A value is required for bind parameter '1'" before the fix
1239
+ test_data = [
1240
+ {"id": 1, "json_data": '{"key1":"value1", "key2":123}'},
1241
+ {"id": 2, "json_data": '{"a":1, "b":"red", "c":{"nested":"value"}}'},
1242
+ {"id": 3, "json_data": '["item1", "item2", "item3"]'},
1243
+ {"id": 4, "json_data": '{"中文":"測試", "number":456}'},
1244
+ ]
1245
+
1246
+ # This should NOT raise "A value is required for bind parameter" error
1247
+ test_client.batch_insert(JsonColonTest, test_data)
1248
+
1249
+ # Verify all rows were inserted
1250
+ result = test_client.query(JsonColonTest).execute()
1251
+ rows = result.fetchall()
1252
+ assert len(rows) == 4, "Should insert all 4 rows with JSON containing colons"
1253
+
1254
+ # Test single insert with JSON colons
1255
+ test_client.insert(JsonColonTest, {"id": 5, "json_data": '{"test":"single insert", "value":999}'})
1256
+
1257
+ result = test_client.query(JsonColonTest).execute()
1258
+ rows = result.fetchall()
1259
+ assert len(rows) == 5, "Should have 5 rows after single insert"
1260
+
1261
+ # Test fulltext search on JSON data
1262
+ result = test_client.query(JsonColonTest).filter(boolean_match(JsonColonTest.json_data).must("red")).execute()
1263
+
1264
+ rows = result.fetchall()
1265
+ assert len(rows) >= 1, "Should find JSON with 'red'"
1266
+ assert rows[0].id == 2, "Should find the correct JSON document"
1267
+
1268
+ finally:
1269
+ # Clean up
1270
+ test_client.drop_table(JsonColonTest)
1271
+ test_client.execute("DROP DATABASE fulltext_json_colon_test")
1272
+
1273
+ # ============================================================================
1274
+ # NGRAM Parser Tests (Chinese Content)
1275
+ # ============================================================================
1276
+
1277
+ def test_fulltext_ngram_parser_chinese(self, test_client):
1278
+ """Test NGRAM parser with Chinese content - comprehensive coverage"""
1279
+ test_client.fulltext_index.enable_fulltext()
1280
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_ngram_test")
1281
+ test_client.execute("USE fulltext_ngram_test")
1282
+
1283
+ Base = declarative_base()
1284
+
1285
+ class ChineseArticle(Base):
1286
+ __tablename__ = "chinese_articles"
1287
+ id = Column(Integer, primary_key=True, autoincrement=True)
1288
+ title = Column(String(200))
1289
+ body = Column(Text)
1290
+ __table_args__ = (FulltextIndex("ftidx_ngram", ["title", "body"], parser=FulltextParserType.NGRAM),)
1291
+
1292
+ try:
1293
+ test_client.create_table(ChineseArticle)
1294
+
1295
+ # Insert Chinese content
1296
+ articles = [
1297
+ {"id": 1, "title": "神雕侠侣 第一回 风月无情", "body": "越女采莲秋水畔,窄袖轻罗,暗露双金钏"},
1298
+ {
1299
+ "id": 2,
1300
+ "title": "神雕侠侣 第二回 故人之子",
1301
+ "body": "正自发痴,忽听左首屋中传出一人喝道:这是在人家府上,你又提小龙女干什么?",
1302
+ },
1303
+ {"id": 3, "title": "神雕侠侣 第三回 投师终南", "body": "郭靖在舟中潜运神功,数日间伤势便已痊愈了大半。"},
1304
+ {
1305
+ "id": 4,
1306
+ "title": "神雕侠侣 第四回 全真门下",
1307
+ "body": "郭靖摆脱众道纠缠,提气向重阳宫奔去,忽听得钟声镗镗响起",
1308
+ },
1309
+ {"id": 5, "title": "神雕侠侣 第五回 活死人墓", "body": "杨过摔下山坡,滚入树林长草丛中,便即昏晕"},
1310
+ {"id": 6, "title": "神雕侠侣 第六回 玉女心经", "body": "小龙女从怀里取出一个瓷瓶,交在杨过手里"},
1311
+ ]
1312
+ test_client.batch_insert(ChineseArticle, articles)
1313
+
1314
+ # Test NGRAM search for Chinese terms (using natural language mode)
1315
+ result = (
1316
+ test_client.query(ChineseArticle)
1317
+ .filter(natural_match(ChineseArticle.title, ChineseArticle.body, query="风月无情"))
1318
+ .execute()
1319
+ )
1320
+ rows = result.fetchall()
1321
+ assert len(rows) >= 1, "Should find articles with 风月无情"
1322
+ assert rows[0].id == 1, "Should find the correct article"
1323
+
1324
+ # Test multi-character Chinese search
1325
+ result = (
1326
+ test_client.query(ChineseArticle)
1327
+ .filter(natural_match(ChineseArticle.title, ChineseArticle.body, query="杨过"))
1328
+ .execute()
1329
+ )
1330
+ rows = result.fetchall()
1331
+ assert len(rows) >= 2, "Should find multiple articles with 杨过"
1332
+ found_ids = {row.id for row in rows}
1333
+ assert 5 in found_ids and 6 in found_ids, "Should find articles 5 and 6"
1334
+
1335
+ # Test search for common term across all articles
1336
+ result = (
1337
+ test_client.query(ChineseArticle)
1338
+ .filter(natural_match(ChineseArticle.title, ChineseArticle.body, query="神雕侠侣"))
1339
+ .execute()
1340
+ )
1341
+ rows = result.fetchall()
1342
+ assert len(rows) == 6, "Should find all 6 articles with 神雕侠侣 in title"
1343
+
1344
+ # Test 小龙女 search
1345
+ result = (
1346
+ test_client.query(ChineseArticle)
1347
+ .filter(natural_match(ChineseArticle.title, ChineseArticle.body, query="小龙女"))
1348
+ .execute()
1349
+ )
1350
+ rows = result.fetchall()
1351
+ assert len(rows) >= 2, "Should find articles mentioning 小龙女"
1352
+ found_ids = {row.id for row in rows}
1353
+ assert 2 in found_ids and 6 in found_ids, "Should find articles 2 and 6"
1354
+
1355
+ finally:
1356
+ test_client.drop_table(ChineseArticle)
1357
+ test_client.execute("DROP DATABASE fulltext_ngram_test")
1358
+
1359
+ def test_fulltext_ngram_parser_mixed_content(self, test_client):
1360
+ """Test NGRAM parser with mixed English and Chinese content"""
1361
+ test_client.fulltext_index.enable_fulltext()
1362
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_ngram_mixed_test")
1363
+ test_client.execute("USE fulltext_ngram_mixed_test")
1364
+
1365
+ Base = declarative_base()
1366
+
1367
+ class MixedArticle(Base):
1368
+ __tablename__ = "mixed_articles"
1369
+ id = Column(Integer, primary_key=True, autoincrement=True)
1370
+ title = Column(String(255))
1371
+ content = Column(Text)
1372
+ __table_args__ = (FulltextIndex("ftidx_ngram_mixed", ["title", "content"], parser=FulltextParserType.NGRAM),)
1373
+
1374
+ try:
1375
+ test_client.create_table(MixedArticle)
1376
+
1377
+ # Insert mixed content
1378
+ articles = [
1379
+ {
1380
+ "id": 1,
1381
+ "title": "MO全文索引示例",
1382
+ "content": "这是一个关于MO全文索引的例子。它展示了如何使用ngram解析器进行全文搜索。",
1383
+ },
1384
+ {"id": 2, "title": "ngram解析器", "content": "ngram解析器允许MO对中文等语言进行分词,以优化全文搜索。"},
1385
+ ]
1386
+ test_client.batch_insert(MixedArticle, articles)
1387
+
1388
+ # Test search for Chinese term (using natural language mode)
1389
+ result = (
1390
+ test_client.query(MixedArticle)
1391
+ .filter(natural_match(MixedArticle.title, MixedArticle.content, query="全文索引"))
1392
+ .execute()
1393
+ )
1394
+ rows = result.fetchall()
1395
+ assert len(rows) >= 1, "Should find articles with 全文索引"
1396
+ assert rows[0].id == 1, "Should find article 1"
1397
+
1398
+ # Test search for English term (using natural language mode)
1399
+ result = (
1400
+ test_client.query(MixedArticle)
1401
+ .filter(natural_match(MixedArticle.title, MixedArticle.content, query="ngram"))
1402
+ .execute()
1403
+ )
1404
+ rows = result.fetchall()
1405
+ assert len(rows) >= 1, "Should find articles with ngram"
1406
+
1407
+ finally:
1408
+ test_client.drop_table(MixedArticle)
1409
+ test_client.execute("DROP DATABASE fulltext_ngram_mixed_test")
1410
+
1411
+ # ============================================================================
1412
+ # BM25 Algorithm Tests
1413
+ # ============================================================================
1414
+
1415
+ def test_fulltext_bm25_algorithm(self, test_client):
1416
+ """Test BM25 relevancy algorithm"""
1417
+ test_client.fulltext_index.enable_fulltext()
1418
+ test_client.execute('SET ft_relevancy_algorithm = "BM25"')
1419
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_bm25_test")
1420
+ test_client.execute("USE fulltext_bm25_test")
1421
+
1422
+ Base = declarative_base()
1423
+
1424
+ class BM25Article(Base):
1425
+ __tablename__ = "bm25_articles"
1426
+ id = Column(Integer, primary_key=True)
1427
+ body = Column(String(500))
1428
+ title = Column(Text)
1429
+ __table_args__ = (FulltextIndex("ftidx_bm25", ["body", "title"]),)
1430
+
1431
+ try:
1432
+ test_client.create_table(BM25Article)
1433
+
1434
+ # Insert test data
1435
+ articles = [
1436
+ {"id": 0, "body": "color is red", "title": "t1"},
1437
+ {"id": 1, "body": "car is yellow", "title": "crazy car"},
1438
+ {"id": 2, "body": "sky is blue", "title": "no limit"},
1439
+ {"id": 3, "body": "blue is not red", "title": "colorful"},
1440
+ ]
1441
+ test_client.batch_insert(BM25Article, articles)
1442
+
1443
+ # Test basic BM25 search
1444
+ result = (
1445
+ test_client.query(BM25Article)
1446
+ .filter(boolean_match(BM25Article.body, BM25Article.title).must("red"))
1447
+ .execute()
1448
+ )
1449
+ rows = result.fetchall()
1450
+ assert len(rows) == 2, "Should find 2 articles with 'red'"
1451
+ found_ids = {row.id for row in rows}
1452
+ assert 0 in found_ids and 3 in found_ids, "Should find articles 0 and 3"
1453
+
1454
+ # Test BM25 with multiple terms
1455
+ result = (
1456
+ test_client.query(BM25Article)
1457
+ .filter(boolean_match(BM25Article.body, BM25Article.title).must("blue", "red"))
1458
+ .execute()
1459
+ )
1460
+ rows = result.fetchall()
1461
+ assert len(rows) >= 1, "Should find articles with both blue and red"
1462
+ assert rows[0].id == 3, "Should find article 3"
1463
+
1464
+ finally:
1465
+ test_client.drop_table(BM25Article)
1466
+ test_client.execute("DROP DATABASE fulltext_bm25_test")
1467
+ # Reset to TF-IDF
1468
+ test_client.execute('SET ft_relevancy_algorithm = "TF-IDF"')
1469
+
1470
+ # ============================================================================
1471
+ # Complex Boolean Mode Operators
1472
+ # ============================================================================
1473
+
1474
+ def test_fulltext_boolean_wildcard(self, test_client):
1475
+ """Test boolean mode wildcard operator"""
1476
+ test_client.fulltext_index.enable_fulltext()
1477
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_wildcard_test")
1478
+ test_client.execute("USE fulltext_wildcard_test")
1479
+
1480
+ Base = declarative_base()
1481
+
1482
+ class WildcardTest(Base):
1483
+ __tablename__ = "wildcard_test"
1484
+ id = Column(Integer, primary_key=True)
1485
+ body = Column(String(500))
1486
+ title = Column(Text)
1487
+ __table_args__ = (FulltextIndex("ftidx_wildcard", ["body", "title"]),)
1488
+
1489
+ try:
1490
+ test_client.create_table(WildcardTest)
1491
+
1492
+ articles = [
1493
+ {"id": 0, "body": "color is red", "title": "t1"},
1494
+ {"id": 1, "body": "car is yellow", "title": "crazy car"},
1495
+ {"id": 2, "body": "sky is blue", "title": "no limit"},
1496
+ {"id": 3, "body": "blue is not red", "title": "colorful"},
1497
+ ]
1498
+ test_client.batch_insert(WildcardTest, articles)
1499
+
1500
+ # Test wildcard: re* should match 'red'
1501
+ # Note: Using raw SQL as wildcard may not be directly supported in ORM API
1502
+ result = test_client.execute(
1503
+ "SELECT * FROM wildcard_test WHERE MATCH(body, title) AGAINST('re*' IN BOOLEAN MODE)"
1504
+ )
1505
+ rows = result.fetchall()
1506
+ assert len(rows) == 2, "Wildcard re* should match 'red'"
1507
+
1508
+ finally:
1509
+ test_client.drop_table(WildcardTest)
1510
+ test_client.execute("DROP DATABASE fulltext_wildcard_test")
1511
+
1512
+ def test_fulltext_boolean_phrase(self, test_client):
1513
+ """Test boolean mode phrase search"""
1514
+ test_client.fulltext_index.enable_fulltext()
1515
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_phrase_test")
1516
+ test_client.execute("USE fulltext_phrase_test")
1517
+
1518
+ Base = declarative_base()
1519
+
1520
+ class PhraseTest(Base):
1521
+ __tablename__ = "phrase_test"
1522
+ id = Column(Integer, primary_key=True)
1523
+ body = Column(String(500))
1524
+ title = Column(Text)
1525
+ __table_args__ = (FulltextIndex("ftidx_phrase", ["body", "title"]),)
1526
+
1527
+ try:
1528
+ test_client.create_table(PhraseTest)
1529
+
1530
+ articles = [
1531
+ {"id": 0, "body": "color is red", "title": "t1"},
1532
+ {"id": 1, "body": "car is yellow", "title": "crazy car"},
1533
+ {"id": 2, "body": "sky is blue", "title": "no limit"},
1534
+ {"id": 3, "body": "blue is not red", "title": "colorful"},
1535
+ ]
1536
+ test_client.batch_insert(PhraseTest, articles)
1537
+
1538
+ # Test phrase search: "is not red" should match exact phrase
1539
+ result = test_client.execute(
1540
+ 'SELECT * FROM phrase_test WHERE MATCH(body, title) AGAINST(\'"is not red"\' IN BOOLEAN MODE)'
1541
+ )
1542
+ rows = result.fetchall()
1543
+ assert len(rows) == 1, "Should find exact phrase 'is not red'"
1544
+ assert rows[0][0] == 3, "Should find article 3"
1545
+
1546
+ # Test non-matching phrase
1547
+ result = test_client.execute(
1548
+ 'SELECT * FROM phrase_test WHERE MATCH(body, title) AGAINST(\'"blue is red"\' IN BOOLEAN MODE)'
1549
+ )
1550
+ rows = result.fetchall()
1551
+ assert len(rows) == 0, "Should not find non-existent phrase"
1552
+
1553
+ finally:
1554
+ test_client.drop_table(PhraseTest)
1555
+ test_client.execute("DROP DATABASE fulltext_phrase_test")
1556
+
1557
+ # ============================================================================
1558
+ # UPDATE/DELETE Index Maintenance Tests
1559
+ # ============================================================================
1560
+
1561
+ def test_fulltext_update_maintenance(self, test_client):
1562
+ """Test that fulltext index is maintained after UPDATE"""
1563
+ test_client.fulltext_index.enable_fulltext()
1564
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_update_test")
1565
+ test_client.execute("USE fulltext_update_test")
1566
+
1567
+ Base = declarative_base()
1568
+
1569
+ class UpdateTest(Base):
1570
+ __tablename__ = "update_test"
1571
+ id = Column(Integer, primary_key=True)
1572
+ body = Column(String(500))
1573
+ title = Column(Text)
1574
+ __table_args__ = (FulltextIndex("ftidx_update", ["body", "title"]),)
1575
+
1576
+ try:
1577
+ test_client.create_table(UpdateTest)
1578
+
1579
+ articles = [
1580
+ {"id": 0, "body": "color is red", "title": "t1"},
1581
+ {"id": 1, "body": "car is yellow", "title": "crazy car"},
1582
+ ]
1583
+ test_client.batch_insert(UpdateTest, articles)
1584
+
1585
+ # Search for 'red' - should find article 0
1586
+ result = test_client.execute("SELECT * FROM update_test WHERE MATCH(body, title) AGAINST('red')")
1587
+ rows = result.fetchall()
1588
+ assert len(rows) == 1 and rows[0][0] == 0, "Should find article 0 with 'red'"
1589
+
1590
+ # Update article 0 to have 'brown' instead of 'red'
1591
+ test_client.execute("UPDATE update_test SET body='color is brown' WHERE id=0")
1592
+
1593
+ # Search for 'red' - should find nothing
1594
+ result = test_client.execute("SELECT * FROM update_test WHERE MATCH(body, title) AGAINST('red')")
1595
+ rows = result.fetchall()
1596
+ assert len(rows) == 0, "Should not find 'red' after update"
1597
+
1598
+ # Search for 'brown' - should find updated article
1599
+ result = test_client.execute("SELECT * FROM update_test WHERE MATCH(body, title) AGAINST('brown')")
1600
+ rows = result.fetchall()
1601
+ assert len(rows) == 1 and rows[0][0] == 0, "Should find updated article with 'brown'"
1602
+
1603
+ finally:
1604
+ test_client.drop_table(UpdateTest)
1605
+ test_client.execute("DROP DATABASE fulltext_update_test")
1606
+
1607
+ def test_fulltext_delete_maintenance(self, test_client):
1608
+ """Test that fulltext index is maintained after DELETE"""
1609
+ test_client.fulltext_index.enable_fulltext()
1610
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_delete_test")
1611
+ test_client.execute("USE fulltext_delete_test")
1612
+
1613
+ Base = declarative_base()
1614
+
1615
+ class DeleteTest(Base):
1616
+ __tablename__ = "delete_test"
1617
+ id = Column(Integer, primary_key=True)
1618
+ body = Column(String(500))
1619
+ title = Column(Text)
1620
+ __table_args__ = (FulltextIndex("ftidx_delete", ["body", "title"]),)
1621
+
1622
+ try:
1623
+ test_client.create_table(DeleteTest)
1624
+
1625
+ articles = [
1626
+ {"id": 0, "body": "red", "title": "t1"},
1627
+ {"id": 1, "body": "yellow", "title": "t2"},
1628
+ {"id": 2, "body": "blue", "title": "t3"},
1629
+ {"id": 3, "body": "blue red", "title": "t4"},
1630
+ ]
1631
+ test_client.batch_insert(DeleteTest, articles)
1632
+
1633
+ # Search for 'red' - should find 2 articles
1634
+ result = test_client.execute("SELECT * FROM delete_test WHERE MATCH(body, title) AGAINST('red')")
1635
+ rows = result.fetchall()
1636
+ assert len(rows) == 2, "Should find 2 articles with 'red'"
1637
+
1638
+ # Delete article 3
1639
+ test_client.execute("DELETE FROM delete_test WHERE id=3")
1640
+
1641
+ # Search for 'red' - should find only article 0
1642
+ result = test_client.execute("SELECT * FROM delete_test WHERE MATCH(body, title) AGAINST('red')")
1643
+ rows = result.fetchall()
1644
+ assert len(rows) == 1 and rows[0][0] == 0, "Should find only article 0 after delete"
1645
+
1646
+ finally:
1647
+ test_client.drop_table(DeleteTest)
1648
+ test_client.execute("DROP DATABASE fulltext_delete_test")
1649
+
1650
+ # ============================================================================
1651
+ # ALTER TABLE Tests
1652
+ # ============================================================================
1653
+
1654
+ def test_fulltext_alter_table_drop_column(self, test_client):
1655
+ """Test ALTER TABLE DROP COLUMN with fulltext index"""
1656
+ test_client.fulltext_index.enable_fulltext()
1657
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_alter_test")
1658
+ test_client.execute("USE fulltext_alter_test")
1659
+
1660
+ Base = declarative_base()
1661
+
1662
+ class AlterTest(Base):
1663
+ __tablename__ = "alter_test"
1664
+ employee_number = Column(Integer, primary_key=True)
1665
+ last_name = Column(String(50))
1666
+ first_name = Column(String(50))
1667
+ email = Column(String(100))
1668
+
1669
+ try:
1670
+ test_client.create_table(AlterTest)
1671
+
1672
+ # Insert test data
1673
+ employees = [
1674
+ {"employee_number": 1002, "last_name": "Murphy", "first_name": "Diane", "email": "dmurphy@test.com"},
1675
+ {"employee_number": 1056, "last_name": "Patterson", "first_name": "Mary", "email": "mpatterso@test.com"},
1676
+ ]
1677
+ test_client.batch_insert(AlterTest, employees)
1678
+
1679
+ # Create fulltext index
1680
+ test_client.execute("CREATE FULLTEXT INDEX f01 ON alter_test (last_name, first_name)")
1681
+
1682
+ # Drop a column that's part of the fulltext index
1683
+ test_client.execute("ALTER TABLE alter_test DROP COLUMN last_name")
1684
+
1685
+ # Verify the table structure
1686
+ result = test_client.execute("SHOW CREATE TABLE alter_test")
1687
+ create_stmt = result.fetchone()[1]
1688
+ assert "first_name" in create_stmt, "first_name should still exist"
1689
+ assert "last_name" not in create_stmt, "last_name should be dropped"
1690
+ assert "FULLTEXT" in create_stmt, "Fulltext index should still exist (on remaining column)"
1691
+
1692
+ # Verify data is still accessible
1693
+ result = test_client.execute("SELECT COUNT(*) FROM alter_test")
1694
+ count = result.fetchone()[0]
1695
+ assert count == 2, "All data should still be present"
1696
+
1697
+ finally:
1698
+ test_client.execute("DROP TABLE IF EXISTS alter_test")
1699
+ test_client.execute("DROP DATABASE fulltext_alter_test")
1700
+
1701
+ # ============================================================================
1702
+ # NULL and Edge Case Tests
1703
+ # ============================================================================
1704
+
1705
+ def test_fulltext_null_handling(self, test_client):
1706
+ """Test fulltext index with NULL values"""
1707
+ test_client.fulltext_index.enable_fulltext()
1708
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_null_test")
1709
+ test_client.execute("USE fulltext_null_test")
1710
+
1711
+ Base = declarative_base()
1712
+
1713
+ class NullTest(Base):
1714
+ __tablename__ = "null_test"
1715
+ id = Column(Integer, primary_key=True)
1716
+ body = Column(String(500))
1717
+ title = Column(Text)
1718
+ __table_args__ = (FulltextIndex("ftidx_null", ["body", "title"]),)
1719
+
1720
+ try:
1721
+ test_client.create_table(NullTest)
1722
+
1723
+ # Insert data with NULL values
1724
+ test_client.execute(
1725
+ "INSERT INTO null_test VALUES (0, 'color is red', 't1'), (1, NULL, 'NOT INCLUDED'), "
1726
+ "(2, 'NOT INCLUDED BODY', NULL), (3, NULL, NULL)"
1727
+ )
1728
+
1729
+ # Search should work even with NULLs
1730
+ result = test_client.execute("SELECT * FROM null_test WHERE MATCH(body, title) AGAINST('red')")
1731
+ rows = result.fetchall()
1732
+ assert len(rows) == 1 and rows[0][0] == 0, "Should find article 0 with 'red', ignoring NULLs"
1733
+
1734
+ # NULL-only row should not be found
1735
+ result = test_client.execute("SELECT * FROM null_test WHERE MATCH(body, title) AGAINST('NULL')")
1736
+ rows = result.fetchall()
1737
+ # NULL values should not be indexed
1738
+
1739
+ finally:
1740
+ test_client.drop_table(NullTest)
1741
+ test_client.execute("DROP DATABASE fulltext_null_test")
1742
+
1743
+ def test_fulltext_empty_search_string(self, test_client):
1744
+ """Test fulltext with empty and special search strings"""
1745
+ test_client.fulltext_index.enable_fulltext()
1746
+ test_client.execute("CREATE DATABASE IF NOT EXISTS fulltext_edge_test")
1747
+ test_client.execute("USE fulltext_edge_test")
1748
+
1749
+ Base = declarative_base()
1750
+
1751
+ class EdgeTest(Base):
1752
+ __tablename__ = "edge_test"
1753
+ id = Column(Integer, primary_key=True)
1754
+ data = Column(Text)
1755
+ __table_args__ = (FulltextIndex("ftidx_edge", "data"),)
1756
+
1757
+ try:
1758
+ test_client.create_table(EdgeTest)
1759
+
1760
+ articles = [
1761
+ {"id": 1, "data": "test content"},
1762
+ {"id": 2, "data": "another test"},
1763
+ ]
1764
+ test_client.batch_insert(EdgeTest, articles)
1765
+
1766
+ # Test with empty space - should return nothing
1767
+ result = test_client.execute("SELECT * FROM edge_test WHERE MATCH(data) AGAINST(' ')")
1768
+ rows = result.fetchall()
1769
+ # Empty string search should return no results
1770
+
1771
+ finally:
1772
+ test_client.drop_table(EdgeTest)
1773
+ test_client.execute("DROP DATABASE fulltext_edge_test")