matrixone-python-sdk 0.1.4__py3-none-any.whl → 0.1.5__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/async_client.py +27 -11
- matrixone/cli_tools.py +1895 -0
- matrixone/client.py +126 -12
- matrixone/connection_hooks.py +54 -5
- matrixone/index_utils.py +4 -2
- {matrixone_python_sdk-0.1.4.dist-info → matrixone_python_sdk-0.1.5.dist-info}/METADATA +347 -6
- {matrixone_python_sdk-0.1.4.dist-info → matrixone_python_sdk-0.1.5.dist-info}/RECORD +12 -10
- {matrixone_python_sdk-0.1.4.dist-info → matrixone_python_sdk-0.1.5.dist-info}/entry_points.txt +1 -1
- tests/online/test_cli_tools_online.py +482 -0
- {matrixone_python_sdk-0.1.4.dist-info → matrixone_python_sdk-0.1.5.dist-info}/WHEEL +0 -0
- {matrixone_python_sdk-0.1.4.dist-info → matrixone_python_sdk-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {matrixone_python_sdk-0.1.4.dist-info → matrixone_python_sdk-0.1.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,482 @@
|
|
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
|
+
Online tests for MatrixOne CLI diagnostic tool
|
19
|
+
"""
|
20
|
+
|
21
|
+
import pytest
|
22
|
+
import io
|
23
|
+
import sys
|
24
|
+
from contextlib import redirect_stdout
|
25
|
+
from matrixone import Client
|
26
|
+
from matrixone.cli_tools import MatrixOneCLI
|
27
|
+
from .test_config import online_config
|
28
|
+
|
29
|
+
|
30
|
+
@pytest.fixture(scope="module")
|
31
|
+
def client():
|
32
|
+
"""Create a MatrixOne client for testing"""
|
33
|
+
c = Client()
|
34
|
+
c.connect(
|
35
|
+
host=online_config.host,
|
36
|
+
port=online_config.port,
|
37
|
+
user=online_config.user,
|
38
|
+
password=online_config.password,
|
39
|
+
database=online_config.database,
|
40
|
+
)
|
41
|
+
yield c
|
42
|
+
c.disconnect()
|
43
|
+
|
44
|
+
|
45
|
+
@pytest.fixture(scope="module")
|
46
|
+
def cli_instance(client):
|
47
|
+
"""Create a CLI instance for testing"""
|
48
|
+
cli = MatrixOneCLI(client)
|
49
|
+
cli.current_database = online_config.database
|
50
|
+
return cli
|
51
|
+
|
52
|
+
|
53
|
+
@pytest.fixture(scope="module")
|
54
|
+
def test_table(client):
|
55
|
+
"""Create a test table with vector index for CLI testing"""
|
56
|
+
table_name = "cli_test_table"
|
57
|
+
|
58
|
+
# Clean up if exists
|
59
|
+
try:
|
60
|
+
client.execute(f"DROP TABLE IF EXISTS {table_name}")
|
61
|
+
except:
|
62
|
+
pass
|
63
|
+
|
64
|
+
# Create table with vector column
|
65
|
+
client.create_table(
|
66
|
+
table_name, {"id": "int", "name": "varchar(100)", "embedding": "vecf32(128)", "content": "text"}, primary_key="id"
|
67
|
+
)
|
68
|
+
|
69
|
+
# Insert test data
|
70
|
+
import random
|
71
|
+
|
72
|
+
test_data = []
|
73
|
+
for i in range(100):
|
74
|
+
test_data.append(
|
75
|
+
{
|
76
|
+
"id": i,
|
77
|
+
"name": f"item_{i}",
|
78
|
+
"embedding": [random.random() for _ in range(128)],
|
79
|
+
"content": f"test content {i}",
|
80
|
+
}
|
81
|
+
)
|
82
|
+
|
83
|
+
client.batch_insert(table_name, test_data)
|
84
|
+
|
85
|
+
# Flush table to ensure metadata is available
|
86
|
+
try:
|
87
|
+
client.moctl.flush_table(online_config.database, table_name)
|
88
|
+
except:
|
89
|
+
pass # Flush might not be available in all environments
|
90
|
+
|
91
|
+
# Create IVF index
|
92
|
+
try:
|
93
|
+
client.vector_ops.create_ivf_index(
|
94
|
+
table_name=table_name, column_name="embedding", index_name="idx_embedding_ivf", lists=10
|
95
|
+
)
|
96
|
+
except:
|
97
|
+
pass # Index creation might fail in some environments
|
98
|
+
|
99
|
+
yield table_name
|
100
|
+
|
101
|
+
# Cleanup
|
102
|
+
try:
|
103
|
+
client.execute(f"DROP TABLE IF EXISTS {table_name}")
|
104
|
+
except:
|
105
|
+
pass
|
106
|
+
|
107
|
+
|
108
|
+
@pytest.fixture(scope="module")
|
109
|
+
def test_table_with_mixed_indexes(client):
|
110
|
+
"""Create a test table with both regular and UNIQUE indexes for testing"""
|
111
|
+
table_name = "cli_test_mixed_indexes"
|
112
|
+
|
113
|
+
# Clean up if exists
|
114
|
+
try:
|
115
|
+
client.execute(f"DROP TABLE IF EXISTS {table_name}")
|
116
|
+
except:
|
117
|
+
pass
|
118
|
+
|
119
|
+
# Create table with multiple columns
|
120
|
+
sql = f"""
|
121
|
+
CREATE TABLE {table_name} (
|
122
|
+
id INT PRIMARY KEY,
|
123
|
+
email VARCHAR(100) UNIQUE,
|
124
|
+
username VARCHAR(50),
|
125
|
+
category VARCHAR(50),
|
126
|
+
status INT,
|
127
|
+
UNIQUE KEY uk_username (username),
|
128
|
+
INDEX idx_category (category),
|
129
|
+
INDEX idx_status (status)
|
130
|
+
)
|
131
|
+
"""
|
132
|
+
client.execute(sql)
|
133
|
+
|
134
|
+
# Insert test data
|
135
|
+
for i in range(50):
|
136
|
+
client.execute(
|
137
|
+
f"INSERT INTO {table_name} VALUES (?, ?, ?, ?, ?)", (i, f"user{i}@test.com", f"user{i}", f"cat{i % 5}", i % 3)
|
138
|
+
)
|
139
|
+
|
140
|
+
# Flush table to ensure metadata is available
|
141
|
+
try:
|
142
|
+
client.moctl.flush_table(online_config.database, table_name)
|
143
|
+
except:
|
144
|
+
pass # Flush might not be available in all environments
|
145
|
+
|
146
|
+
yield table_name
|
147
|
+
|
148
|
+
# Cleanup
|
149
|
+
try:
|
150
|
+
client.execute(f"DROP TABLE IF EXISTS {table_name}")
|
151
|
+
except:
|
152
|
+
pass
|
153
|
+
|
154
|
+
|
155
|
+
class TestCLIBasicCommands:
|
156
|
+
"""Test basic CLI commands"""
|
157
|
+
|
158
|
+
def test_show_all_indexes(self, cli_instance):
|
159
|
+
"""Test show_all_indexes command"""
|
160
|
+
# Capture output
|
161
|
+
f = io.StringIO()
|
162
|
+
with redirect_stdout(f):
|
163
|
+
cli_instance.onecmd("show_all_indexes")
|
164
|
+
|
165
|
+
output = f.getvalue()
|
166
|
+
# Should not error
|
167
|
+
assert "Error" not in output or output == ""
|
168
|
+
|
169
|
+
def test_sql_command(self, cli_instance):
|
170
|
+
"""Test SQL command execution"""
|
171
|
+
f = io.StringIO()
|
172
|
+
with redirect_stdout(f):
|
173
|
+
cli_instance.onecmd("sql SELECT 1")
|
174
|
+
|
175
|
+
output = f.getvalue()
|
176
|
+
assert "1" in output or "returned" in output
|
177
|
+
|
178
|
+
|
179
|
+
class TestCLIIndexCommands:
|
180
|
+
"""Test index-related CLI commands"""
|
181
|
+
|
182
|
+
def test_show_indexes_on_table(self, cli_instance, test_table):
|
183
|
+
"""Test show_indexes command on a specific table"""
|
184
|
+
f = io.StringIO()
|
185
|
+
with redirect_stdout(f):
|
186
|
+
cli_instance.onecmd(f"show_indexes {test_table}")
|
187
|
+
|
188
|
+
output = f.getvalue()
|
189
|
+
# Should either show indexes or indicate none found
|
190
|
+
assert "Error" not in output or "No secondary indexes" in output
|
191
|
+
|
192
|
+
def test_verify_counts(self, cli_instance, test_table):
|
193
|
+
"""Test verify_counts command"""
|
194
|
+
f = io.StringIO()
|
195
|
+
with redirect_stdout(f):
|
196
|
+
cli_instance.onecmd(f"verify_counts {test_table}")
|
197
|
+
|
198
|
+
output = f.getvalue()
|
199
|
+
# Should either verify successfully or indicate no indexes
|
200
|
+
assert "PASSED" in output or "No secondary indexes" in output or "Error" not in output
|
201
|
+
|
202
|
+
def test_show_indexes_with_mixed_types(self, cli_instance, test_table_with_mixed_indexes):
|
203
|
+
"""Test show_indexes command on a table with both regular and UNIQUE indexes"""
|
204
|
+
f = io.StringIO()
|
205
|
+
with redirect_stdout(f):
|
206
|
+
cli_instance.onecmd(f"show_indexes {test_table_with_mixed_indexes}")
|
207
|
+
|
208
|
+
output = f.getvalue()
|
209
|
+
# Should show both regular and unique indexes
|
210
|
+
assert "Secondary Indexes" in output
|
211
|
+
# Should show multiple index tables (2 UNIQUE + 2 regular = 4 total)
|
212
|
+
assert "index tables" in output.lower() or "indexes" in output.lower()
|
213
|
+
|
214
|
+
def test_verify_counts_with_mixed_indexes(self, cli_instance, test_table_with_mixed_indexes):
|
215
|
+
"""Test verify_counts on table with both regular and UNIQUE indexes"""
|
216
|
+
f = io.StringIO()
|
217
|
+
with redirect_stdout(f):
|
218
|
+
cli_instance.onecmd(f"verify_counts {test_table_with_mixed_indexes}")
|
219
|
+
|
220
|
+
output = f.getvalue()
|
221
|
+
# Should verify all index tables including UNIQUE
|
222
|
+
assert "PASSED" in output or "50 rows" in output
|
223
|
+
# Should show verification for unique indexes
|
224
|
+
assert "__mo_index_unique_" in output or "__mo_index_secondary_" in output
|
225
|
+
|
226
|
+
|
227
|
+
class TestCLIIVFCommands:
|
228
|
+
"""Test IVF index related commands"""
|
229
|
+
|
230
|
+
def test_show_ivf_status(self, cli_instance):
|
231
|
+
"""Test show_ivf_status command"""
|
232
|
+
f = io.StringIO()
|
233
|
+
with redirect_stdout(f):
|
234
|
+
cli_instance.onecmd("show_ivf_status")
|
235
|
+
|
236
|
+
output = f.getvalue()
|
237
|
+
# Should either show IVF indexes or indicate none found
|
238
|
+
assert "Error" not in output or "No IVF indexes" in output
|
239
|
+
|
240
|
+
def test_show_ivf_status_with_table_filter(self, cli_instance, test_table):
|
241
|
+
"""Test show_ivf_status with table filter"""
|
242
|
+
f = io.StringIO()
|
243
|
+
with redirect_stdout(f):
|
244
|
+
cli_instance.onecmd(f"show_ivf_status -t {test_table}")
|
245
|
+
|
246
|
+
output = f.getvalue()
|
247
|
+
# Should complete without critical errors
|
248
|
+
assert "❌" not in output or "No IVF indexes" in output
|
249
|
+
|
250
|
+
|
251
|
+
class TestCLITableStatsCommands:
|
252
|
+
"""Test table statistics commands"""
|
253
|
+
|
254
|
+
def test_show_table_stats_basic(self, cli_instance, test_table):
|
255
|
+
"""Test basic table stats"""
|
256
|
+
f = io.StringIO()
|
257
|
+
with redirect_stdout(f):
|
258
|
+
cli_instance.onecmd(f"show_table_stats {test_table}")
|
259
|
+
|
260
|
+
output = f.getvalue()
|
261
|
+
# Should show table statistics or indicate no stats available
|
262
|
+
assert "Table Statistics" in output or "Objects" in output or "No statistics available" in output
|
263
|
+
|
264
|
+
def test_show_table_stats_with_tombstone(self, cli_instance, test_table):
|
265
|
+
"""Test table stats with tombstone"""
|
266
|
+
f = io.StringIO()
|
267
|
+
with redirect_stdout(f):
|
268
|
+
cli_instance.onecmd(f"show_table_stats {test_table} -t")
|
269
|
+
|
270
|
+
output = f.getvalue()
|
271
|
+
# Should include statistics or indicate no stats available
|
272
|
+
assert "Table Statistics" in output or "Objects" in output or "No statistics available" in output
|
273
|
+
|
274
|
+
def test_show_table_stats_detailed(self, cli_instance, test_table):
|
275
|
+
"""Test detailed table stats"""
|
276
|
+
f = io.StringIO()
|
277
|
+
with redirect_stdout(f):
|
278
|
+
cli_instance.onecmd(f"show_table_stats {test_table} -d")
|
279
|
+
|
280
|
+
output = f.getvalue()
|
281
|
+
# Should show detailed object list or indicate no stats available
|
282
|
+
assert "Detailed Table Statistics" in output or "Object Name" in output or "No statistics available" in output
|
283
|
+
|
284
|
+
def test_show_table_stats_all(self, cli_instance, test_table):
|
285
|
+
"""Test table stats with all options"""
|
286
|
+
f = io.StringIO()
|
287
|
+
with redirect_stdout(f):
|
288
|
+
cli_instance.onecmd(f"show_table_stats {test_table} -a")
|
289
|
+
|
290
|
+
output = f.getvalue()
|
291
|
+
# Should show comprehensive statistics or indicate no stats available
|
292
|
+
assert "Table Statistics" in output or "Objects" in output or "No statistics available" in output
|
293
|
+
|
294
|
+
|
295
|
+
class TestCLINonInteractiveMode:
|
296
|
+
"""Test non-interactive command execution"""
|
297
|
+
|
298
|
+
def test_single_command_execution(self, client):
|
299
|
+
"""Test executing a single command via onecmd"""
|
300
|
+
cli = MatrixOneCLI(client)
|
301
|
+
cli.current_database = online_config.database
|
302
|
+
|
303
|
+
# Capture output
|
304
|
+
f = io.StringIO()
|
305
|
+
with redirect_stdout(f):
|
306
|
+
cli.onecmd("sql SELECT 1 as test_value")
|
307
|
+
|
308
|
+
output = f.getvalue()
|
309
|
+
assert "test_value" in output or "1" in output
|
310
|
+
|
311
|
+
def test_show_all_indexes_non_interactive(self, client):
|
312
|
+
"""Test show_all_indexes in non-interactive mode"""
|
313
|
+
cli = MatrixOneCLI(client)
|
314
|
+
cli.current_database = online_config.database
|
315
|
+
|
316
|
+
f = io.StringIO()
|
317
|
+
with redirect_stdout(f):
|
318
|
+
cli.onecmd("show_all_indexes")
|
319
|
+
|
320
|
+
output = f.getvalue()
|
321
|
+
# Should complete without critical errors
|
322
|
+
assert "❌ Error" not in output or output == ""
|
323
|
+
|
324
|
+
|
325
|
+
class TestCLIErrorHandling:
|
326
|
+
"""Test CLI error handling"""
|
327
|
+
|
328
|
+
def test_invalid_table_name(self, cli_instance):
|
329
|
+
"""Test handling of invalid table name"""
|
330
|
+
f = io.StringIO()
|
331
|
+
with redirect_stdout(f):
|
332
|
+
cli_instance.onecmd("show_table_stats nonexistent_table_xyz_123")
|
333
|
+
|
334
|
+
output = f.getvalue()
|
335
|
+
# Should show error or no statistics message
|
336
|
+
assert "Error" in output or "No statistics" in output
|
337
|
+
|
338
|
+
def test_empty_command(self, cli_instance):
|
339
|
+
"""Test handling of empty commands"""
|
340
|
+
# Should not crash
|
341
|
+
cli_instance.onecmd("")
|
342
|
+
assert True # If we get here, it didn't crash
|
343
|
+
|
344
|
+
def test_malformed_sql(self, cli_instance):
|
345
|
+
"""Test handling of malformed SQL"""
|
346
|
+
f = io.StringIO()
|
347
|
+
with redirect_stdout(f):
|
348
|
+
cli_instance.onecmd("sql SELECT * FROM") # Incomplete SQL
|
349
|
+
|
350
|
+
output = f.getvalue()
|
351
|
+
# Should show error
|
352
|
+
assert "Error" in output or "❌" in output
|
353
|
+
|
354
|
+
|
355
|
+
class TestCLIFlushCommands:
|
356
|
+
"""Test flush table commands"""
|
357
|
+
|
358
|
+
def test_flush_table_basic(self, cli_instance, test_table):
|
359
|
+
"""Test basic flush table command"""
|
360
|
+
f = io.StringIO()
|
361
|
+
with redirect_stdout(f):
|
362
|
+
cli_instance.onecmd(f"flush_table {test_table}")
|
363
|
+
|
364
|
+
output = f.getvalue()
|
365
|
+
# Should attempt to flush (may fail due to permissions, but should not crash)
|
366
|
+
assert "Flushing table" in output or "Error" in output or "Failed" in output or "flushed" in output
|
367
|
+
|
368
|
+
def test_flush_table_with_database(self, cli_instance, test_table):
|
369
|
+
"""Test flush table with database parameter"""
|
370
|
+
f = io.StringIO()
|
371
|
+
with redirect_stdout(f):
|
372
|
+
cli_instance.onecmd(f"flush_table {test_table} test")
|
373
|
+
|
374
|
+
output = f.getvalue()
|
375
|
+
# Should attempt to flush (may fail due to permissions, but should not crash)
|
376
|
+
assert "Flushing table" in output or "Error" in output or "Failed" in output or "flushed" in output
|
377
|
+
|
378
|
+
def test_flush_table_invalid_table(self, cli_instance):
|
379
|
+
"""Test flush table with invalid table name"""
|
380
|
+
f = io.StringIO()
|
381
|
+
with redirect_stdout(f):
|
382
|
+
cli_instance.onecmd("flush_table nonexistent_table")
|
383
|
+
|
384
|
+
output = f.getvalue()
|
385
|
+
# Should show error for invalid table
|
386
|
+
assert "Error" in output or "Failed" in output
|
387
|
+
|
388
|
+
def test_flush_table_no_args(self, cli_instance):
|
389
|
+
"""Test flush table without arguments"""
|
390
|
+
f = io.StringIO()
|
391
|
+
with redirect_stdout(f):
|
392
|
+
cli_instance.onecmd("flush_table")
|
393
|
+
|
394
|
+
output = f.getvalue()
|
395
|
+
# Should show usage error
|
396
|
+
assert "Error" in output and "required" in output
|
397
|
+
|
398
|
+
def test_flush_table_with_mixed_indexes(self, cli_instance, test_table_with_mixed_indexes):
|
399
|
+
"""Test flush table with both regular and UNIQUE indexes"""
|
400
|
+
f = io.StringIO()
|
401
|
+
with redirect_stdout(f):
|
402
|
+
cli_instance.onecmd(f"flush_table {test_table_with_mixed_indexes}")
|
403
|
+
|
404
|
+
output = f.getvalue()
|
405
|
+
# Should flush main table and all index tables (UNIQUE + regular)
|
406
|
+
assert "Flushing table" in output
|
407
|
+
# Should show count of index tables (4 = 2 UNIQUE + 2 regular)
|
408
|
+
assert "index tables" in output.lower() or "flushed" in output.lower()
|
409
|
+
|
410
|
+
|
411
|
+
class TestCLIUtilityCommands:
|
412
|
+
"""Test utility commands"""
|
413
|
+
|
414
|
+
def test_tables_command(self, cli_instance):
|
415
|
+
"""Test tables command"""
|
416
|
+
f = io.StringIO()
|
417
|
+
with redirect_stdout(f):
|
418
|
+
cli_instance.onecmd("tables")
|
419
|
+
|
420
|
+
output = f.getvalue()
|
421
|
+
# Should show tables in current database
|
422
|
+
assert "Tables in database" in output or "No tables" in output or "Total:" in output
|
423
|
+
|
424
|
+
def test_tables_with_database(self, cli_instance):
|
425
|
+
"""Test tables command with database parameter"""
|
426
|
+
f = io.StringIO()
|
427
|
+
with redirect_stdout(f):
|
428
|
+
cli_instance.onecmd("tables test")
|
429
|
+
|
430
|
+
output = f.getvalue()
|
431
|
+
# Should show tables in specified database
|
432
|
+
assert "Tables in database" in output or "No tables" in output or "Total:" in output
|
433
|
+
|
434
|
+
def test_databases_command(self, cli_instance):
|
435
|
+
"""Test databases command"""
|
436
|
+
f = io.StringIO()
|
437
|
+
with redirect_stdout(f):
|
438
|
+
cli_instance.onecmd("databases")
|
439
|
+
|
440
|
+
output = f.getvalue()
|
441
|
+
# Should show databases
|
442
|
+
assert "Databases:" in output and "Total:" in output
|
443
|
+
# Should show at least 'test' database
|
444
|
+
assert "test" in output.lower() or "mo_catalog" in output.lower()
|
445
|
+
|
446
|
+
|
447
|
+
class TestCLIHelp:
|
448
|
+
"""Test CLI help functionality"""
|
449
|
+
|
450
|
+
def test_help_command(self, cli_instance):
|
451
|
+
"""Test help command"""
|
452
|
+
# Redirect both stdout and cli_instance.stdout
|
453
|
+
f = io.StringIO()
|
454
|
+
old_stdout = cli_instance.stdout
|
455
|
+
cli_instance.stdout = f
|
456
|
+
|
457
|
+
try:
|
458
|
+
cli_instance.onecmd("help")
|
459
|
+
output = f.getvalue()
|
460
|
+
# Should show available commands
|
461
|
+
assert len(output) > 0
|
462
|
+
finally:
|
463
|
+
cli_instance.stdout = old_stdout
|
464
|
+
|
465
|
+
def test_help_specific_command(self, cli_instance):
|
466
|
+
"""Test help for specific command"""
|
467
|
+
# Redirect both stdout and cli_instance.stdout
|
468
|
+
f = io.StringIO()
|
469
|
+
old_stdout = cli_instance.stdout
|
470
|
+
cli_instance.stdout = f
|
471
|
+
|
472
|
+
try:
|
473
|
+
cli_instance.onecmd("help show_table_stats")
|
474
|
+
output = f.getvalue()
|
475
|
+
# Should show help for show_table_stats (help output goes to self.stdout)
|
476
|
+
assert len(output) > 0
|
477
|
+
finally:
|
478
|
+
cli_instance.stdout = old_stdout
|
479
|
+
|
480
|
+
|
481
|
+
if __name__ == '__main__':
|
482
|
+
pytest.main([__file__, '-v'])
|
File without changes
|
{matrixone_python_sdk-0.1.4.dist-info → matrixone_python_sdk-0.1.5.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|