matrixone-python-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrixone/__init__.py +155 -0
- matrixone/account.py +723 -0
- matrixone/async_client.py +3913 -0
- matrixone/async_metadata_manager.py +311 -0
- matrixone/async_orm.py +123 -0
- matrixone/async_vector_index_manager.py +633 -0
- matrixone/base_client.py +208 -0
- matrixone/client.py +4672 -0
- matrixone/config.py +452 -0
- matrixone/connection_hooks.py +286 -0
- matrixone/exceptions.py +89 -0
- matrixone/logger.py +782 -0
- matrixone/metadata.py +820 -0
- matrixone/moctl.py +219 -0
- matrixone/orm.py +2277 -0
- matrixone/pitr.py +646 -0
- matrixone/pubsub.py +771 -0
- matrixone/restore.py +411 -0
- matrixone/search_vector_index.py +1176 -0
- matrixone/snapshot.py +550 -0
- matrixone/sql_builder.py +844 -0
- matrixone/sqlalchemy_ext/__init__.py +161 -0
- matrixone/sqlalchemy_ext/adapters.py +163 -0
- matrixone/sqlalchemy_ext/dialect.py +534 -0
- matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
- matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
- matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
- matrixone/sqlalchemy_ext/ivf_config.py +252 -0
- matrixone/sqlalchemy_ext/table_builder.py +351 -0
- matrixone/sqlalchemy_ext/vector_index.py +1721 -0
- matrixone/sqlalchemy_ext/vector_type.py +948 -0
- matrixone/version.py +580 -0
- matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
- matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
- matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
- matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
- matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +19 -0
- tests/offline/__init__.py +20 -0
- tests/offline/conftest.py +77 -0
- tests/offline/test_account.py +703 -0
- tests/offline/test_async_client_query_comprehensive.py +1218 -0
- tests/offline/test_basic.py +54 -0
- tests/offline/test_case_sensitivity.py +227 -0
- tests/offline/test_connection_hooks_offline.py +287 -0
- tests/offline/test_dialect_schema_handling.py +609 -0
- tests/offline/test_explain_methods.py +346 -0
- tests/offline/test_filter_logical_in.py +237 -0
- tests/offline/test_fulltext_search_comprehensive.py +795 -0
- tests/offline/test_ivf_config.py +249 -0
- tests/offline/test_join_methods.py +281 -0
- tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
- tests/offline/test_logical_in_method.py +237 -0
- tests/offline/test_matrixone_version_parsing.py +264 -0
- tests/offline/test_metadata_offline.py +557 -0
- tests/offline/test_moctl.py +300 -0
- tests/offline/test_moctl_simple.py +251 -0
- tests/offline/test_model_support_offline.py +359 -0
- tests/offline/test_model_support_simple.py +225 -0
- tests/offline/test_pinecone_filter_offline.py +377 -0
- tests/offline/test_pitr.py +585 -0
- tests/offline/test_pubsub.py +712 -0
- tests/offline/test_query_update.py +283 -0
- tests/offline/test_restore.py +445 -0
- tests/offline/test_snapshot_comprehensive.py +384 -0
- tests/offline/test_sql_escaping_edge_cases.py +551 -0
- tests/offline/test_sqlalchemy_integration.py +382 -0
- tests/offline/test_sqlalchemy_vector_integration.py +434 -0
- tests/offline/test_table_builder.py +198 -0
- tests/offline/test_unified_filter.py +398 -0
- tests/offline/test_unified_transaction.py +495 -0
- tests/offline/test_vector_index.py +238 -0
- tests/offline/test_vector_operations.py +688 -0
- tests/offline/test_vector_type.py +174 -0
- tests/offline/test_version_core.py +328 -0
- tests/offline/test_version_management.py +372 -0
- tests/offline/test_version_standalone.py +652 -0
- tests/online/__init__.py +20 -0
- tests/online/conftest.py +216 -0
- tests/online/test_account_management.py +194 -0
- tests/online/test_advanced_features.py +344 -0
- tests/online/test_async_client_interfaces.py +330 -0
- tests/online/test_async_client_online.py +285 -0
- tests/online/test_async_model_insert_online.py +293 -0
- tests/online/test_async_orm_online.py +300 -0
- tests/online/test_async_simple_query_online.py +802 -0
- tests/online/test_async_transaction_simple_query.py +300 -0
- tests/online/test_basic_connection.py +130 -0
- tests/online/test_client_online.py +238 -0
- tests/online/test_config.py +90 -0
- tests/online/test_config_validation.py +123 -0
- tests/online/test_connection_hooks_new_online.py +217 -0
- tests/online/test_dialect_schema_handling_online.py +331 -0
- tests/online/test_filter_logical_in_online.py +374 -0
- tests/online/test_fulltext_comprehensive.py +1773 -0
- tests/online/test_fulltext_label_online.py +433 -0
- tests/online/test_fulltext_search_online.py +842 -0
- tests/online/test_ivf_stats_online.py +506 -0
- tests/online/test_logger_integration.py +311 -0
- tests/online/test_matrixone_query_orm.py +540 -0
- tests/online/test_metadata_online.py +579 -0
- tests/online/test_model_insert_online.py +255 -0
- tests/online/test_mysql_driver_validation.py +213 -0
- tests/online/test_orm_advanced_features.py +2022 -0
- tests/online/test_orm_cte_integration.py +269 -0
- tests/online/test_orm_online.py +270 -0
- tests/online/test_pinecone_filter.py +708 -0
- tests/online/test_pubsub_operations.py +352 -0
- tests/online/test_query_methods.py +225 -0
- tests/online/test_query_update_online.py +433 -0
- tests/online/test_search_vector_index.py +557 -0
- tests/online/test_simple_fulltext_online.py +915 -0
- tests/online/test_snapshot_comprehensive.py +998 -0
- tests/online/test_sqlalchemy_engine_integration.py +336 -0
- tests/online/test_sqlalchemy_integration.py +425 -0
- tests/online/test_transaction_contexts.py +1219 -0
- tests/online/test_transaction_insert_methods.py +356 -0
- tests/online/test_transaction_query_methods.py +288 -0
- tests/online/test_unified_filter_online.py +529 -0
- tests/online/test_vector_comprehensive.py +706 -0
- tests/online/test_version_management.py +291 -0
@@ -0,0 +1,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")
|