pyseekdb 0.1.0.dev3__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.
- pyseekdb/__init__.py +90 -0
- pyseekdb/client/__init__.py +324 -0
- pyseekdb/client/admin_client.py +202 -0
- pyseekdb/client/base_connection.py +82 -0
- pyseekdb/client/client_base.py +1921 -0
- pyseekdb/client/client_oceanbase_server.py +258 -0
- pyseekdb/client/client_seekdb_embedded.py +324 -0
- pyseekdb/client/client_seekdb_server.py +226 -0
- pyseekdb/client/collection.py +485 -0
- pyseekdb/client/database.py +55 -0
- pyseekdb/client/filters.py +357 -0
- pyseekdb/client/meta_info.py +15 -0
- pyseekdb/client/query_result.py +122 -0
- pyseekdb/client/sql_utils.py +48 -0
- pyseekdb/examples/comprehensive_example.py +412 -0
- pyseekdb/examples/simple_example.py +113 -0
- pyseekdb/tests/__init__.py +0 -0
- pyseekdb/tests/test_admin_database_management.py +307 -0
- pyseekdb/tests/test_client_creation.py +425 -0
- pyseekdb/tests/test_collection_dml.py +652 -0
- pyseekdb/tests/test_collection_get.py +550 -0
- pyseekdb/tests/test_collection_hybrid_search.py +1126 -0
- pyseekdb/tests/test_collection_query.py +428 -0
- pyseekdb-0.1.0.dev3.dist-info/LICENSE +202 -0
- pyseekdb-0.1.0.dev3.dist-info/METADATA +856 -0
- pyseekdb-0.1.0.dev3.dist-info/RECORD +27 -0
- pyseekdb-0.1.0.dev3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Collection get tests - testing collection.get() interface for all three modes
|
|
3
|
+
Supports configuring connection parameters via environment variables
|
|
4
|
+
"""
|
|
5
|
+
import pytest
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import json
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Add project path
|
|
14
|
+
project_root = Path(__file__).parent.parent.parent
|
|
15
|
+
sys.path.insert(0, str(project_root))
|
|
16
|
+
|
|
17
|
+
import seekdbclient
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ==================== Environment Variable Configuration ====================
|
|
21
|
+
# Embedded mode
|
|
22
|
+
SEEKDB_PATH = os.environ.get('SEEKDB_PATH', os.path.join(project_root, "seekdb_store"))
|
|
23
|
+
SEEKDB_DATABASE = os.environ.get('SEEKDB_DATABASE', 'test')
|
|
24
|
+
|
|
25
|
+
# Server mode
|
|
26
|
+
SERVER_HOST = os.environ.get('SERVER_HOST', 'localhost')
|
|
27
|
+
SERVER_PORT = int(os.environ.get('SERVER_PORT', '2881'))
|
|
28
|
+
SERVER_DATABASE = os.environ.get('SERVER_DATABASE', 'test')
|
|
29
|
+
SERVER_USER = os.environ.get('SERVER_USER', 'root')
|
|
30
|
+
SERVER_PASSWORD = os.environ.get('SERVER_PASSWORD', '')
|
|
31
|
+
|
|
32
|
+
# OceanBase mode
|
|
33
|
+
OB_HOST = os.environ.get('OB_HOST', 'localhost')
|
|
34
|
+
OB_PORT = int(os.environ.get('OB_PORT', '11202'))
|
|
35
|
+
OB_TENANT = os.environ.get('OB_TENANT', 'mysql')
|
|
36
|
+
OB_DATABASE = os.environ.get('OB_DATABASE', 'test')
|
|
37
|
+
OB_USER = os.environ.get('OB_USER', 'root')
|
|
38
|
+
OB_PASSWORD = os.environ.get('OB_PASSWORD', '')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestCollectionGet:
|
|
42
|
+
"""Test collection.get() interface for all three modes"""
|
|
43
|
+
|
|
44
|
+
def _insert_test_data(self, client, collection_name: str):
|
|
45
|
+
"""Helper method to insert test data and return inserted IDs"""
|
|
46
|
+
table_name = f"c$v1${collection_name}"
|
|
47
|
+
|
|
48
|
+
# Insert test data with vectors, documents, and metadata
|
|
49
|
+
test_data = [
|
|
50
|
+
{
|
|
51
|
+
"_id": str(uuid.uuid4()),
|
|
52
|
+
"document": "This is a test document about machine learning",
|
|
53
|
+
"embedding": [1.0, 2.0, 3.0],
|
|
54
|
+
"metadata": {"category": "AI", "score": 95, "tag": "ml"}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"_id": str(uuid.uuid4()),
|
|
58
|
+
"document": "Python programming tutorial for beginners",
|
|
59
|
+
"embedding": [2.0, 3.0, 4.0],
|
|
60
|
+
"metadata": {"category": "Programming", "score": 88, "tag": "python"}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"_id": str(uuid.uuid4()),
|
|
64
|
+
"document": "Advanced machine learning algorithms",
|
|
65
|
+
"embedding": [1.1, 2.1, 3.1],
|
|
66
|
+
"metadata": {"category": "AI", "score": 92, "tag": "ml"}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"_id": str(uuid.uuid4()),
|
|
70
|
+
"document": "Data science with Python",
|
|
71
|
+
"embedding": [2.1, 3.1, 4.1],
|
|
72
|
+
"metadata": {"category": "Data Science", "score": 90, "tag": "python"}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"_id": str(uuid.uuid4()),
|
|
76
|
+
"document": "Introduction to neural networks",
|
|
77
|
+
"embedding": [1.2, 2.2, 3.2],
|
|
78
|
+
"metadata": {"category": "AI", "score": 85, "tag": "neural"}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Store inserted IDs for return (using generated UUIDs)
|
|
83
|
+
inserted_ids = []
|
|
84
|
+
|
|
85
|
+
# Insert all data with generated UUIDs
|
|
86
|
+
for data in test_data:
|
|
87
|
+
# Use string ID directly (support any string format)
|
|
88
|
+
id_str = data["_id"]
|
|
89
|
+
inserted_ids.append(id_str) # Store original ID string for return
|
|
90
|
+
|
|
91
|
+
# Escape single quotes in ID
|
|
92
|
+
id_str_escaped = id_str.replace("'", "''")
|
|
93
|
+
|
|
94
|
+
# Convert vector to string format: [1.0,2.0,3.0]
|
|
95
|
+
vector_str = "[" + ",".join(map(str, data["embedding"])) + "]"
|
|
96
|
+
# Convert metadata to JSON string
|
|
97
|
+
metadata_str = json.dumps(data["metadata"]).replace("'", "\\'")
|
|
98
|
+
# Escape single quotes in document
|
|
99
|
+
document_str = data["document"].replace("'", "\\'")
|
|
100
|
+
|
|
101
|
+
# Use CAST to convert string to binary for varbinary(512) field
|
|
102
|
+
sql = f"""INSERT INTO `{table_name}` (_id, document, embedding, metadata)
|
|
103
|
+
VALUES (CAST('{id_str_escaped}' AS BINARY), '{document_str}', '{vector_str}', '{metadata_str}')"""
|
|
104
|
+
client._server.execute(sql)
|
|
105
|
+
|
|
106
|
+
return inserted_ids
|
|
107
|
+
|
|
108
|
+
def test_embedded_collection_get(self):
|
|
109
|
+
"""Test collection.get() with embedded client"""
|
|
110
|
+
if not os.path.exists(SEEKDB_PATH):
|
|
111
|
+
pytest.skip(
|
|
112
|
+
f"SeekDB data directory does not exist: {SEEKDB_PATH}\n"
|
|
113
|
+
f"Set SEEKDB_PATH environment variable to run this test"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Check if seekdb package is available
|
|
117
|
+
try:
|
|
118
|
+
import seekdb
|
|
119
|
+
except ImportError:
|
|
120
|
+
pytest.skip("SeekDB embedded package is not installed")
|
|
121
|
+
|
|
122
|
+
# Create embedded client
|
|
123
|
+
client = seekdbclient.Client(
|
|
124
|
+
path=SEEKDB_PATH,
|
|
125
|
+
database=SEEKDB_DATABASE
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
assert client is not None
|
|
129
|
+
assert hasattr(client, '_server')
|
|
130
|
+
assert isinstance(client._server, seekdbclient.SeekdbEmbeddedClient)
|
|
131
|
+
|
|
132
|
+
# Create test collection
|
|
133
|
+
collection_name = f"test_get_{int(time.time())}"
|
|
134
|
+
collection = client.create_collection(name=collection_name, dimension=3)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Insert test data and get IDs
|
|
138
|
+
inserted_ids = self._insert_test_data(client, collection_name)
|
|
139
|
+
assert len(inserted_ids) > 0, f"Failed to get inserted IDs. Expected at least 1, got {len(inserted_ids)}"
|
|
140
|
+
if len(inserted_ids) < 5:
|
|
141
|
+
print(f" Warning: Expected 5 inserted IDs, but got {len(inserted_ids)}")
|
|
142
|
+
|
|
143
|
+
# Test 1: Get by single ID
|
|
144
|
+
print(f"\n✅ Testing get by single ID for embedded client")
|
|
145
|
+
results = collection.get(ids=inserted_ids[0])
|
|
146
|
+
assert results is not None
|
|
147
|
+
assert len(results) == 1
|
|
148
|
+
print(f" Found {len(results)} result for ID={inserted_ids[0]}")
|
|
149
|
+
|
|
150
|
+
# Test 2: Get by multiple IDs
|
|
151
|
+
print(f"✅ Testing get by multiple IDs")
|
|
152
|
+
if len(inserted_ids) >= 2:
|
|
153
|
+
results = collection.get(ids=inserted_ids[:2])
|
|
154
|
+
assert results is not None
|
|
155
|
+
assert len(results) <= 2
|
|
156
|
+
print(f" Found {len(results)} results for IDs={inserted_ids[:2]}")
|
|
157
|
+
|
|
158
|
+
# Test 3: Get by metadata filter
|
|
159
|
+
print(f"✅ Testing get with metadata filter")
|
|
160
|
+
results = collection.get(
|
|
161
|
+
where={"category": {"$eq": "AI"}},
|
|
162
|
+
limit=10
|
|
163
|
+
)
|
|
164
|
+
assert results is not None
|
|
165
|
+
assert len(results) > 0
|
|
166
|
+
print(f" Found {len(results)} results with category='AI'")
|
|
167
|
+
|
|
168
|
+
# Test 4: Get by document filter
|
|
169
|
+
print(f"✅ Testing get with document filter")
|
|
170
|
+
results = collection.get(
|
|
171
|
+
where_document={"$contains": "machine learning"},
|
|
172
|
+
limit=10
|
|
173
|
+
)
|
|
174
|
+
assert results is not None
|
|
175
|
+
print(f" Found {len(results)} results containing 'machine learning'")
|
|
176
|
+
|
|
177
|
+
# Test 5: Get with include parameter
|
|
178
|
+
print(f"✅ Testing get with include parameter")
|
|
179
|
+
results = collection.get(
|
|
180
|
+
ids=inserted_ids[:2],
|
|
181
|
+
include=["documents", "metadatas"]
|
|
182
|
+
)
|
|
183
|
+
assert results is not None
|
|
184
|
+
assert isinstance(results, list), "Multiple IDs should return List[QueryResult]"
|
|
185
|
+
assert len(results) == 2, f"Expected 2 QueryResult objects, got {len(results)}"
|
|
186
|
+
for i, result in enumerate(results):
|
|
187
|
+
assert isinstance(result, seekdbclient.QueryResult), f"Result {i} should be QueryResult"
|
|
188
|
+
if len(result) > 0:
|
|
189
|
+
for item in result:
|
|
190
|
+
result_dict = item.to_dict() if hasattr(item, 'to_dict') else item
|
|
191
|
+
assert '_id' in result_dict
|
|
192
|
+
print(f" QueryResult {i} keys: {list(result[0].to_dict().keys()) if len(result) > 0 else 'empty'}")
|
|
193
|
+
|
|
194
|
+
# Test 6: Get all data with limit
|
|
195
|
+
print(f"✅ Testing get all data with limit")
|
|
196
|
+
results = collection.get(limit=3)
|
|
197
|
+
assert results is not None
|
|
198
|
+
assert len(results) <= 3
|
|
199
|
+
print(f" Found {len(results)} results (limit=3)")
|
|
200
|
+
|
|
201
|
+
# Test 7: Get by multiple IDs (should return List[QueryResult])
|
|
202
|
+
print(f"✅ Testing get by multiple IDs (returns List[QueryResult])")
|
|
203
|
+
if len(inserted_ids) >= 3:
|
|
204
|
+
results = collection.get(ids=inserted_ids[:3])
|
|
205
|
+
assert results is not None
|
|
206
|
+
assert isinstance(results, list), "Multiple IDs should return List[QueryResult]"
|
|
207
|
+
assert len(results) == 3, f"Expected 3 QueryResult objects, got {len(results)}"
|
|
208
|
+
for i, result in enumerate(results):
|
|
209
|
+
assert isinstance(result, seekdbclient.QueryResult), f"Result {i} should be QueryResult"
|
|
210
|
+
assert len(result) >= 0, f"QueryResult {i} should exist (may be empty if ID not found)"
|
|
211
|
+
print(f" QueryResult {i} for ID {inserted_ids[i]}: {len(result)} items")
|
|
212
|
+
|
|
213
|
+
# Test 8: Single ID still returns single QueryResult (backward compatibility)
|
|
214
|
+
print(f"✅ Testing single ID returns single QueryResult (backward compatibility)")
|
|
215
|
+
results = collection.get(ids=inserted_ids[0])
|
|
216
|
+
assert results is not None
|
|
217
|
+
assert isinstance(results, seekdbclient.QueryResult), "Single ID should return QueryResult, not list"
|
|
218
|
+
assert len(results) == 1
|
|
219
|
+
print(f" Single QueryResult with {len(results)} item")
|
|
220
|
+
|
|
221
|
+
finally:
|
|
222
|
+
# Cleanup
|
|
223
|
+
try:
|
|
224
|
+
client.delete_collection(name=collection_name)
|
|
225
|
+
print(f" Cleaned up collection: {collection_name}")
|
|
226
|
+
except Exception as cleanup_error:
|
|
227
|
+
print(f" Warning: Failed to cleanup collection: {cleanup_error}")
|
|
228
|
+
|
|
229
|
+
def test_server_collection_get(self):
|
|
230
|
+
"""Test collection.get() with server client"""
|
|
231
|
+
# Create server client
|
|
232
|
+
client = seekdbclient.Client(
|
|
233
|
+
host=SERVER_HOST,
|
|
234
|
+
port=SERVER_PORT,
|
|
235
|
+
database=SERVER_DATABASE,
|
|
236
|
+
user=SERVER_USER,
|
|
237
|
+
password=SERVER_PASSWORD
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
assert client is not None
|
|
241
|
+
assert hasattr(client, '_server')
|
|
242
|
+
assert isinstance(client._server, seekdbclient.SeekdbServerClient)
|
|
243
|
+
|
|
244
|
+
# Test connection
|
|
245
|
+
try:
|
|
246
|
+
result = client._server.execute("SELECT 1 as test")
|
|
247
|
+
assert result is not None
|
|
248
|
+
except Exception as e:
|
|
249
|
+
pytest.skip(f"Server connection failed ({SERVER_HOST}:{SERVER_PORT}): {e}")
|
|
250
|
+
|
|
251
|
+
# Create test collection
|
|
252
|
+
collection_name = f"test_get_{int(time.time())}"
|
|
253
|
+
collection = client.create_collection(name=collection_name, dimension=3)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
# Insert test data and get IDs
|
|
257
|
+
inserted_ids = self._insert_test_data(client, collection_name)
|
|
258
|
+
assert len(inserted_ids) > 0, f"Failed to get inserted IDs. Expected at least 1, got {len(inserted_ids)}"
|
|
259
|
+
if len(inserted_ids) < 5:
|
|
260
|
+
print(f" Warning: Expected 5 inserted IDs, but got {len(inserted_ids)}")
|
|
261
|
+
|
|
262
|
+
# Test 1: Get by single ID
|
|
263
|
+
print(f"\n✅ Testing get by single ID for server client")
|
|
264
|
+
results = collection.get(ids=inserted_ids[0])
|
|
265
|
+
assert results is not None
|
|
266
|
+
assert len(results) == 1
|
|
267
|
+
print(f" Found {len(results)} result for ID={inserted_ids[0]}")
|
|
268
|
+
|
|
269
|
+
# Test 2: Get by multiple IDs
|
|
270
|
+
print(f"✅ Testing get by multiple IDs")
|
|
271
|
+
if len(inserted_ids) >= 3:
|
|
272
|
+
results = collection.get(ids=inserted_ids[:3])
|
|
273
|
+
assert results is not None
|
|
274
|
+
assert len(results) <= 3
|
|
275
|
+
print(f" Found {len(results)} results for IDs={inserted_ids[:3]}")
|
|
276
|
+
|
|
277
|
+
# Test 3: Get by metadata filter with comparison operator
|
|
278
|
+
print(f"✅ Testing get with metadata filter ($gte)")
|
|
279
|
+
results = collection.get(
|
|
280
|
+
where={"score": {"$gte": 90}},
|
|
281
|
+
limit=10
|
|
282
|
+
)
|
|
283
|
+
assert results is not None
|
|
284
|
+
assert len(results) > 0
|
|
285
|
+
print(f" Found {len(results)} results with score >= 90")
|
|
286
|
+
|
|
287
|
+
# Test 4: Get by combined metadata filters
|
|
288
|
+
print(f"✅ Testing get with combined metadata filters")
|
|
289
|
+
results = collection.get(
|
|
290
|
+
where={"category": {"$eq": "AI"}, "score": {"$gte": 90}},
|
|
291
|
+
limit=10
|
|
292
|
+
)
|
|
293
|
+
assert results is not None
|
|
294
|
+
print(f" Found {len(results)} results with category='AI' and score >= 90")
|
|
295
|
+
|
|
296
|
+
# Test 5: Get by document filter
|
|
297
|
+
print(f"✅ Testing get with document filter")
|
|
298
|
+
results = collection.get(
|
|
299
|
+
where_document={"$contains": "Python"},
|
|
300
|
+
limit=10
|
|
301
|
+
)
|
|
302
|
+
assert results is not None
|
|
303
|
+
print(f" Found {len(results)} results containing 'Python'")
|
|
304
|
+
|
|
305
|
+
# Test 6: Get with $in operator
|
|
306
|
+
print(f"✅ Testing get with $in operator")
|
|
307
|
+
results = collection.get(
|
|
308
|
+
where={"tag": {"$in": ["ml", "python"]}},
|
|
309
|
+
limit=10
|
|
310
|
+
)
|
|
311
|
+
assert results is not None
|
|
312
|
+
print(f" Found {len(results)} results with tag in ['ml', 'python']")
|
|
313
|
+
|
|
314
|
+
# Test 7: Get with limit and offset
|
|
315
|
+
print(f"✅ Testing get with limit and offset")
|
|
316
|
+
results = collection.get(limit=2, offset=1)
|
|
317
|
+
assert results is not None
|
|
318
|
+
assert len(results) <= 2
|
|
319
|
+
print(f" Found {len(results)} results (limit=2, offset=1)")
|
|
320
|
+
|
|
321
|
+
# Test 8: Get with include parameter
|
|
322
|
+
print(f"✅ Testing get with include parameter")
|
|
323
|
+
results = collection.get(
|
|
324
|
+
ids=inserted_ids[:2],
|
|
325
|
+
include=["documents", "metadatas", "embeddings"]
|
|
326
|
+
)
|
|
327
|
+
assert results is not None
|
|
328
|
+
assert isinstance(results, list), "Multiple IDs should return List[QueryResult]"
|
|
329
|
+
assert len(results) == 2, f"Expected 2 QueryResult objects, got {len(results)}"
|
|
330
|
+
for i, result in enumerate(results):
|
|
331
|
+
assert isinstance(result, seekdbclient.QueryResult), f"Result {i} should be QueryResult"
|
|
332
|
+
if len(result) > 0:
|
|
333
|
+
for item in result:
|
|
334
|
+
result_dict = item.to_dict() if hasattr(item, 'to_dict') else item
|
|
335
|
+
assert '_id' in result_dict
|
|
336
|
+
print(f" QueryResult {i} keys: {list(result[0].to_dict().keys()) if len(result) > 0 else 'empty'}")
|
|
337
|
+
|
|
338
|
+
# Test 9: Get by multiple IDs (should return List[QueryResult])
|
|
339
|
+
print(f"✅ Testing get by multiple IDs (returns List[QueryResult])")
|
|
340
|
+
if len(inserted_ids) >= 3:
|
|
341
|
+
results = collection.get(ids=inserted_ids[:3])
|
|
342
|
+
assert results is not None
|
|
343
|
+
assert isinstance(results, list), "Multiple IDs should return List[QueryResult]"
|
|
344
|
+
assert len(results) == 3, f"Expected 3 QueryResult objects, got {len(results)}"
|
|
345
|
+
for i, result in enumerate(results):
|
|
346
|
+
assert isinstance(result, seekdbclient.QueryResult), f"Result {i} should be QueryResult"
|
|
347
|
+
assert len(result) >= 0, f"QueryResult {i} should exist (may be empty if ID not found)"
|
|
348
|
+
print(f" QueryResult {i} for ID {inserted_ids[i]}: {len(result)} items")
|
|
349
|
+
|
|
350
|
+
# Test 10: Single ID still returns single QueryResult (backward compatibility)
|
|
351
|
+
print(f"✅ Testing single ID returns single QueryResult (backward compatibility)")
|
|
352
|
+
results = collection.get(ids=inserted_ids[0])
|
|
353
|
+
assert results is not None
|
|
354
|
+
assert isinstance(results, seekdbclient.QueryResult), "Single ID should return QueryResult, not list"
|
|
355
|
+
assert len(results) == 1
|
|
356
|
+
print(f" Single QueryResult with {len(results)} item")
|
|
357
|
+
|
|
358
|
+
# Test 11: Get with filters still returns single QueryResult (not multiple)
|
|
359
|
+
print(f"✅ Testing get with filters returns single QueryResult")
|
|
360
|
+
results = collection.get(
|
|
361
|
+
where={"category": {"$eq": "AI"}},
|
|
362
|
+
limit=10
|
|
363
|
+
)
|
|
364
|
+
assert results is not None
|
|
365
|
+
assert isinstance(results, seekdbclient.QueryResult), "Get with filters should return QueryResult, not list"
|
|
366
|
+
print(f" Single QueryResult with {len(results)} items matching filter")
|
|
367
|
+
|
|
368
|
+
finally:
|
|
369
|
+
# Cleanup
|
|
370
|
+
try:
|
|
371
|
+
client.delete_collection(name=collection_name)
|
|
372
|
+
print(f" Cleaned up collection: {collection_name}")
|
|
373
|
+
except Exception as cleanup_error:
|
|
374
|
+
print(f" Warning: Failed to cleanup collection: {cleanup_error}")
|
|
375
|
+
|
|
376
|
+
def test_oceanbase_collection_get(self):
|
|
377
|
+
"""Test collection.get() with OceanBase client"""
|
|
378
|
+
# Create OceanBase client
|
|
379
|
+
client = seekdbclient.OBClient(
|
|
380
|
+
host=OB_HOST,
|
|
381
|
+
port=OB_PORT,
|
|
382
|
+
tenant=OB_TENANT,
|
|
383
|
+
database=OB_DATABASE,
|
|
384
|
+
user=OB_USER,
|
|
385
|
+
password=OB_PASSWORD
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
assert client is not None
|
|
389
|
+
assert hasattr(client, '_server')
|
|
390
|
+
assert isinstance(client._server, seekdbclient.OceanBaseServerClient)
|
|
391
|
+
|
|
392
|
+
# Test connection
|
|
393
|
+
try:
|
|
394
|
+
result = client._server.execute("SELECT 1 as test")
|
|
395
|
+
assert result is not None
|
|
396
|
+
except Exception as e:
|
|
397
|
+
pytest.skip(f"OceanBase connection failed ({OB_HOST}:{OB_PORT}): {e}")
|
|
398
|
+
|
|
399
|
+
# Create test collection
|
|
400
|
+
collection_name = f"test_get_{int(time.time())}"
|
|
401
|
+
collection = client.create_collection(name=collection_name, dimension=3)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
# Insert test data and get IDs
|
|
405
|
+
inserted_ids = self._insert_test_data(client, collection_name)
|
|
406
|
+
assert len(inserted_ids) > 0, f"Failed to get inserted IDs. Expected at least 1, got {len(inserted_ids)}"
|
|
407
|
+
if len(inserted_ids) < 5:
|
|
408
|
+
print(f" Warning: Expected 5 inserted IDs, but got {len(inserted_ids)}")
|
|
409
|
+
|
|
410
|
+
# Test 1: Get by single ID
|
|
411
|
+
print(f"\n✅ Testing get by single ID for OceanBase client")
|
|
412
|
+
results = collection.get(ids=inserted_ids[0])
|
|
413
|
+
assert results is not None
|
|
414
|
+
assert len(results) == 1
|
|
415
|
+
print(f" Found {len(results)} result for ID={inserted_ids[0]}")
|
|
416
|
+
|
|
417
|
+
# Test 2: Get by multiple IDs
|
|
418
|
+
print(f"✅ Testing get by multiple IDs")
|
|
419
|
+
if len(inserted_ids) >= 3:
|
|
420
|
+
results = collection.get(ids=inserted_ids[:3])
|
|
421
|
+
assert results is not None
|
|
422
|
+
assert len(results) <= 3
|
|
423
|
+
print(f" Found {len(results)} results for IDs={inserted_ids[:3]}")
|
|
424
|
+
|
|
425
|
+
# Test 3: Get by metadata filter
|
|
426
|
+
print(f"✅ Testing get with metadata filter")
|
|
427
|
+
results = collection.get(
|
|
428
|
+
where={"category": {"$eq": "AI"}},
|
|
429
|
+
limit=10
|
|
430
|
+
)
|
|
431
|
+
assert results is not None
|
|
432
|
+
assert len(results) > 0
|
|
433
|
+
print(f" Found {len(results)} results with category='AI'")
|
|
434
|
+
|
|
435
|
+
# Test 4: Get by logical operators ($or)
|
|
436
|
+
print(f"✅ Testing get with logical operators ($or)")
|
|
437
|
+
results = collection.get(
|
|
438
|
+
where={
|
|
439
|
+
"$or": [
|
|
440
|
+
{"category": {"$eq": "AI"}},
|
|
441
|
+
{"tag": {"$eq": "python"}}
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
limit=10
|
|
445
|
+
)
|
|
446
|
+
assert results is not None
|
|
447
|
+
print(f" Found {len(results)} results with $or condition")
|
|
448
|
+
|
|
449
|
+
# Test 5: Get by document filter
|
|
450
|
+
print(f"✅ Testing get with document filter")
|
|
451
|
+
results = collection.get(
|
|
452
|
+
where_document={"$contains": "machine"},
|
|
453
|
+
limit=10
|
|
454
|
+
)
|
|
455
|
+
assert results is not None
|
|
456
|
+
print(f" Found {len(results)} results containing 'machine'")
|
|
457
|
+
|
|
458
|
+
# Test 6: Get with combined filters (where + where_document)
|
|
459
|
+
print(f"✅ Testing get with combined filters")
|
|
460
|
+
results = collection.get(
|
|
461
|
+
where={"category": {"$eq": "AI"}},
|
|
462
|
+
where_document={"$contains": "machine"},
|
|
463
|
+
limit=10
|
|
464
|
+
)
|
|
465
|
+
assert results is not None
|
|
466
|
+
print(f" Found {len(results)} results matching all filters")
|
|
467
|
+
|
|
468
|
+
# Test 7: Get with limit and offset
|
|
469
|
+
print(f"✅ Testing get with limit and offset")
|
|
470
|
+
results = collection.get(limit=2, offset=2)
|
|
471
|
+
assert results is not None
|
|
472
|
+
assert len(results) <= 2
|
|
473
|
+
print(f" Found {len(results)} results (limit=2, offset=2)")
|
|
474
|
+
|
|
475
|
+
# Test 8: Get all data without filters
|
|
476
|
+
print(f"✅ Testing get all data without filters")
|
|
477
|
+
results = collection.get(limit=100)
|
|
478
|
+
assert results is not None
|
|
479
|
+
assert len(results) > 0
|
|
480
|
+
print(f" Found {len(results)} total results")
|
|
481
|
+
|
|
482
|
+
# Test 9: Get with include parameter
|
|
483
|
+
print(f"✅ Testing get with include parameter")
|
|
484
|
+
results = collection.get(
|
|
485
|
+
ids=inserted_ids[:2],
|
|
486
|
+
include=["documents", "metadatas", "embeddings"]
|
|
487
|
+
)
|
|
488
|
+
assert results is not None
|
|
489
|
+
assert isinstance(results, list), "Multiple IDs should return List[QueryResult]"
|
|
490
|
+
assert len(results) == 2, f"Expected 2 QueryResult objects, got {len(results)}"
|
|
491
|
+
for i, result in enumerate(results):
|
|
492
|
+
assert isinstance(result, seekdbclient.QueryResult), f"Result {i} should be QueryResult"
|
|
493
|
+
if len(result) > 0:
|
|
494
|
+
for item in result:
|
|
495
|
+
result_dict = item.to_dict() if hasattr(item, 'to_dict') else item
|
|
496
|
+
assert '_id' in result_dict
|
|
497
|
+
print(f" QueryResult {i} keys: {list(result[0].to_dict().keys()) if len(result) > 0 else 'empty'}")
|
|
498
|
+
|
|
499
|
+
# Test 10: Get by multiple IDs (should return List[QueryResult])
|
|
500
|
+
print(f"✅ Testing get by multiple IDs (returns List[QueryResult])")
|
|
501
|
+
if len(inserted_ids) >= 3:
|
|
502
|
+
results = collection.get(ids=inserted_ids[:3])
|
|
503
|
+
assert results is not None
|
|
504
|
+
assert isinstance(results, list), "Multiple IDs should return List[QueryResult]"
|
|
505
|
+
assert len(results) == 3, f"Expected 3 QueryResult objects, got {len(results)}"
|
|
506
|
+
for i, result in enumerate(results):
|
|
507
|
+
assert isinstance(result, seekdbclient.QueryResult), f"Result {i} should be QueryResult"
|
|
508
|
+
assert len(result) >= 0, f"QueryResult {i} should exist (may be empty if ID not found)"
|
|
509
|
+
print(f" QueryResult {i} for ID {inserted_ids[i]}: {len(result)} items")
|
|
510
|
+
|
|
511
|
+
# Test 11: Single ID still returns single QueryResult (backward compatibility)
|
|
512
|
+
print(f"✅ Testing single ID returns single QueryResult (backward compatibility)")
|
|
513
|
+
results = collection.get(ids=inserted_ids[0])
|
|
514
|
+
assert results is not None
|
|
515
|
+
assert isinstance(results, seekdbclient.QueryResult), "Single ID should return QueryResult, not list"
|
|
516
|
+
assert len(results) == 1
|
|
517
|
+
print(f" Single QueryResult with {len(results)} item")
|
|
518
|
+
|
|
519
|
+
# Test 12: Get with filters still returns single QueryResult (not multiple)
|
|
520
|
+
print(f"✅ Testing get with filters returns single QueryResult")
|
|
521
|
+
results = collection.get(
|
|
522
|
+
where={"category": {"$eq": "AI"}},
|
|
523
|
+
limit=10
|
|
524
|
+
)
|
|
525
|
+
assert results is not None
|
|
526
|
+
assert isinstance(results, seekdbclient.QueryResult), "Get with filters should return QueryResult, not list"
|
|
527
|
+
print(f" Single QueryResult with {len(results)} items matching filter")
|
|
528
|
+
|
|
529
|
+
finally:
|
|
530
|
+
# Cleanup
|
|
531
|
+
try:
|
|
532
|
+
client.delete_collection(name=collection_name)
|
|
533
|
+
print(f" Cleaned up collection: {collection_name}")
|
|
534
|
+
except Exception as cleanup_error:
|
|
535
|
+
print(f" Warning: Failed to cleanup collection: {cleanup_error}")
|
|
536
|
+
pass
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
if __name__ == "__main__":
|
|
540
|
+
print("\n" + "="*60)
|
|
541
|
+
print("SeekDBClient - Collection Get Tests")
|
|
542
|
+
print("="*60)
|
|
543
|
+
print(f"\nEnvironment Variable Configuration:")
|
|
544
|
+
print(f" Embedded mode: path={SEEKDB_PATH}, database={SEEKDB_DATABASE}")
|
|
545
|
+
print(f" Server mode: {SERVER_USER}@{SERVER_HOST}:{SERVER_PORT}/{SERVER_DATABASE}")
|
|
546
|
+
print(f" OceanBase mode: {OB_USER}@{OB_TENANT} -> {OB_HOST}:{OB_PORT}/{OB_DATABASE}")
|
|
547
|
+
print("="*60 + "\n")
|
|
548
|
+
|
|
549
|
+
pytest.main([__file__, "-v", "-s"])
|
|
550
|
+
|