matrixone-python-sdk 0.1.3__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 -5
- matrixone/cli_tools.py +1895 -0
- matrixone/client.py +126 -6
- matrixone/connection_hooks.py +61 -12
- matrixone/index_utils.py +42 -18
- {matrixone_python_sdk-0.1.3.dist-info → matrixone_python_sdk-0.1.5.dist-info}/METADATA +347 -6
- {matrixone_python_sdk-0.1.3.dist-info → matrixone_python_sdk-0.1.5.dist-info}/RECORD +13 -11
- {matrixone_python_sdk-0.1.3.dist-info → matrixone_python_sdk-0.1.5.dist-info}/entry_points.txt +1 -1
- tests/offline/test_connection_hooks_offline.py +8 -8
- tests/online/test_cli_tools_online.py +482 -0
- {matrixone_python_sdk-0.1.3.dist-info → matrixone_python_sdk-0.1.5.dist-info}/WHEEL +0 -0
- {matrixone_python_sdk-0.1.3.dist-info → matrixone_python_sdk-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {matrixone_python_sdk-0.1.3.dist-info → matrixone_python_sdk-0.1.5.dist-info}/top_level.txt +0 -0
matrixone/cli_tools.py
ADDED
@@ -0,0 +1,1895 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
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
|
+
MatrixOne Interactive Diagnostic Tool
|
19
|
+
|
20
|
+
A command-line tool for diagnosing and inspecting MatrixOne database objects,
|
21
|
+
especially secondary indexes and vector indexes.
|
22
|
+
"""
|
23
|
+
|
24
|
+
import cmd
|
25
|
+
import logging
|
26
|
+
import sys
|
27
|
+
from pathlib import Path
|
28
|
+
from typing import Optional
|
29
|
+
|
30
|
+
from .client import Client
|
31
|
+
|
32
|
+
# Set default logging level to ERROR to keep output clean
|
33
|
+
# Users can override this with --log-level parameter
|
34
|
+
logging.getLogger('matrixone').setLevel(logging.ERROR)
|
35
|
+
|
36
|
+
# Try to import prompt_toolkit for better input experience
|
37
|
+
try:
|
38
|
+
from prompt_toolkit import PromptSession
|
39
|
+
from prompt_toolkit.completion import Completer, Completion
|
40
|
+
from prompt_toolkit.formatted_text import HTML
|
41
|
+
from prompt_toolkit.history import FileHistory
|
42
|
+
from prompt_toolkit.styles import Style
|
43
|
+
|
44
|
+
PROMPT_TOOLKIT_AVAILABLE = True
|
45
|
+
except ImportError:
|
46
|
+
PROMPT_TOOLKIT_AVAILABLE = False
|
47
|
+
|
48
|
+
|
49
|
+
# Custom completer for mo-diag commands
|
50
|
+
if PROMPT_TOOLKIT_AVAILABLE:
|
51
|
+
|
52
|
+
class MODiagCompleter(Completer):
|
53
|
+
"""Smart completer for mo-diag commands that provides table and database name completion"""
|
54
|
+
|
55
|
+
def __init__(self, cli_instance):
|
56
|
+
self.cli = cli_instance
|
57
|
+
|
58
|
+
def get_completions(self, document, complete_event):
|
59
|
+
"""Generate completions based on current input"""
|
60
|
+
text = document.text_before_cursor
|
61
|
+
words = text.split()
|
62
|
+
|
63
|
+
# Available commands
|
64
|
+
all_commands = [
|
65
|
+
'show_indexes',
|
66
|
+
'show_all_indexes',
|
67
|
+
'verify_counts',
|
68
|
+
'show_ivf_status',
|
69
|
+
'show_table_stats',
|
70
|
+
'flush_table',
|
71
|
+
'tables',
|
72
|
+
'databases',
|
73
|
+
'sql',
|
74
|
+
'use',
|
75
|
+
'connect',
|
76
|
+
'history',
|
77
|
+
'help',
|
78
|
+
'exit',
|
79
|
+
]
|
80
|
+
|
81
|
+
# If empty or only whitespace, suggest commands
|
82
|
+
if not words:
|
83
|
+
for command in all_commands:
|
84
|
+
yield Completion(command, start_position=0)
|
85
|
+
return
|
86
|
+
|
87
|
+
# If we're typing the first word (command), complete it
|
88
|
+
if len(words) == 1 and not text.endswith(' '):
|
89
|
+
partial_command = words[0]
|
90
|
+
for command in all_commands:
|
91
|
+
if command.startswith(partial_command):
|
92
|
+
yield Completion(command, start_position=-len(partial_command))
|
93
|
+
return
|
94
|
+
|
95
|
+
command = words[0]
|
96
|
+
|
97
|
+
# Commands that expect table name as first argument
|
98
|
+
table_commands = ['show_indexes', 'verify_counts', 'show_table_stats', 'flush_table']
|
99
|
+
# Commands that expect database name
|
100
|
+
database_commands = ['use']
|
101
|
+
|
102
|
+
# Determine what to complete
|
103
|
+
if command in table_commands:
|
104
|
+
# If we're on the first argument after the command
|
105
|
+
if len(words) == 1 or (len(words) == 2 and not text.endswith(' ')):
|
106
|
+
# Complete table names
|
107
|
+
partial = words[1] if len(words) == 2 else ''
|
108
|
+
for table in self._get_tables():
|
109
|
+
if table.startswith(partial):
|
110
|
+
yield Completion(table, start_position=-len(partial))
|
111
|
+
# Second argument might be database name
|
112
|
+
elif len(words) == 2 or (len(words) == 3 and not text.endswith(' ')):
|
113
|
+
partial = words[2] if len(words) == 3 else ''
|
114
|
+
for db in self._get_databases():
|
115
|
+
if db.startswith(partial):
|
116
|
+
yield Completion(db, start_position=-len(partial))
|
117
|
+
|
118
|
+
elif command in database_commands:
|
119
|
+
# Complete database names
|
120
|
+
if len(words) == 1 or (len(words) == 2 and not text.endswith(' ')):
|
121
|
+
partial = words[1] if len(words) == 2 else ''
|
122
|
+
for db in self._get_databases():
|
123
|
+
if db.startswith(partial):
|
124
|
+
yield Completion(db, start_position=-len(partial))
|
125
|
+
|
126
|
+
elif command == 'show_ivf_status':
|
127
|
+
# Can take -t <table> or database name
|
128
|
+
if '-t' in words:
|
129
|
+
# After -t, complete table names
|
130
|
+
t_index = words.index('-t')
|
131
|
+
if len(words) == t_index + 1 or (len(words) == t_index + 2 and not text.endswith(' ')):
|
132
|
+
partial = words[t_index + 1] if len(words) == t_index + 2 else ''
|
133
|
+
for table in self._get_tables():
|
134
|
+
if table.startswith(partial):
|
135
|
+
yield Completion(table, start_position=-len(partial))
|
136
|
+
else:
|
137
|
+
# First argument is database name
|
138
|
+
if len(words) == 1 or (len(words) == 2 and not text.endswith(' ')):
|
139
|
+
partial = words[1] if len(words) == 2 else ''
|
140
|
+
for db in self._get_databases():
|
141
|
+
if db.startswith(partial):
|
142
|
+
yield Completion(db, start_position=-len(partial))
|
143
|
+
|
144
|
+
def _get_tables(self):
|
145
|
+
"""Get list of tables in current database"""
|
146
|
+
if not self.cli.client or not self.cli.current_database:
|
147
|
+
return []
|
148
|
+
try:
|
149
|
+
result = self.cli.client.execute("SHOW TABLES")
|
150
|
+
return [row[0] for row in result.fetchall()]
|
151
|
+
except Exception:
|
152
|
+
return []
|
153
|
+
|
154
|
+
def _get_databases(self):
|
155
|
+
"""Get list of all databases"""
|
156
|
+
if not self.cli.client:
|
157
|
+
return []
|
158
|
+
try:
|
159
|
+
result = self.cli.client.execute("SHOW DATABASES")
|
160
|
+
return [row[0] for row in result.fetchall()]
|
161
|
+
except Exception:
|
162
|
+
return []
|
163
|
+
|
164
|
+
|
165
|
+
# ANSI Color codes for terminal output
|
166
|
+
class Colors:
|
167
|
+
"""ANSI color codes"""
|
168
|
+
|
169
|
+
RESET = '\033[0m'
|
170
|
+
BOLD = '\033[1m'
|
171
|
+
|
172
|
+
# Foreground colors
|
173
|
+
RED = '\033[91m'
|
174
|
+
GREEN = '\033[92m'
|
175
|
+
YELLOW = '\033[93m'
|
176
|
+
BLUE = '\033[94m'
|
177
|
+
MAGENTA = '\033[95m'
|
178
|
+
CYAN = '\033[96m'
|
179
|
+
WHITE = '\033[97m'
|
180
|
+
|
181
|
+
# Background colors
|
182
|
+
BG_RED = '\033[101m'
|
183
|
+
BG_GREEN = '\033[102m'
|
184
|
+
BG_YELLOW = '\033[103m'
|
185
|
+
|
186
|
+
@staticmethod
|
187
|
+
def disable():
|
188
|
+
"""Disable colors (for non-terminal output)"""
|
189
|
+
Colors.RESET = ''
|
190
|
+
Colors.BOLD = ''
|
191
|
+
Colors.RED = ''
|
192
|
+
Colors.GREEN = ''
|
193
|
+
Colors.YELLOW = ''
|
194
|
+
Colors.BLUE = ''
|
195
|
+
Colors.MAGENTA = ''
|
196
|
+
Colors.CYAN = ''
|
197
|
+
Colors.WHITE = ''
|
198
|
+
Colors.BG_RED = ''
|
199
|
+
Colors.BG_GREEN = ''
|
200
|
+
Colors.BG_YELLOW = ''
|
201
|
+
|
202
|
+
|
203
|
+
# Check if output is to a terminal
|
204
|
+
if not sys.stdout.isatty():
|
205
|
+
Colors.disable()
|
206
|
+
|
207
|
+
|
208
|
+
def success(msg):
|
209
|
+
"""Print success message in green"""
|
210
|
+
return f"{Colors.GREEN}{msg}{Colors.RESET}"
|
211
|
+
|
212
|
+
|
213
|
+
def error(msg):
|
214
|
+
"""Print error message in red"""
|
215
|
+
return f"{Colors.RED}{msg}{Colors.RESET}"
|
216
|
+
|
217
|
+
|
218
|
+
def warning(msg):
|
219
|
+
"""Print warning message in yellow"""
|
220
|
+
return f"{Colors.YELLOW}{msg}{Colors.RESET}"
|
221
|
+
|
222
|
+
|
223
|
+
def info(msg):
|
224
|
+
"""Print info message in cyan"""
|
225
|
+
return f"{Colors.CYAN}{msg}{Colors.RESET}"
|
226
|
+
|
227
|
+
|
228
|
+
def bold(msg):
|
229
|
+
"""Print message in bold"""
|
230
|
+
return f"{Colors.BOLD}{msg}{Colors.RESET}"
|
231
|
+
|
232
|
+
|
233
|
+
def header(msg):
|
234
|
+
"""Print header in bold cyan"""
|
235
|
+
return f"{Colors.BOLD}{Colors.CYAN}{msg}{Colors.RESET}"
|
236
|
+
|
237
|
+
|
238
|
+
class MatrixOneCLI(cmd.Cmd):
|
239
|
+
"""Interactive CLI for MatrixOne diagnostics"""
|
240
|
+
|
241
|
+
intro = """
|
242
|
+
╔══════════════════════════════════════════════════════════════╗
|
243
|
+
║ MatrixOne Interactive Diagnostic Tool ║
|
244
|
+
║ ║
|
245
|
+
║ Type help or ? to list commands. ║
|
246
|
+
║ Type help <command> for detailed help on a command. ║
|
247
|
+
║ ║
|
248
|
+
║ Tips: ║
|
249
|
+
║ • Press Tab for auto-completion (tables/databases) ║
|
250
|
+
║ • Use ↑/↓ arrows to browse command history ║
|
251
|
+
║ • Press Ctrl+R for history search ║
|
252
|
+
╚══════════════════════════════════════════════════════════════╝
|
253
|
+
"""
|
254
|
+
|
255
|
+
prompt = f'{Colors.BOLD}{Colors.GREEN}MO-DIAG>{Colors.RESET} '
|
256
|
+
|
257
|
+
def __init__(self, client: Optional[Client] = None):
|
258
|
+
"""
|
259
|
+
Initialize the CLI tool.
|
260
|
+
|
261
|
+
Args:
|
262
|
+
client: Optional MatrixOne client. If not provided, you'll need to connect manually.
|
263
|
+
"""
|
264
|
+
super().__init__()
|
265
|
+
self.client = client
|
266
|
+
self.current_database = None
|
267
|
+
|
268
|
+
# Setup prompt_toolkit session if available
|
269
|
+
if PROMPT_TOOLKIT_AVAILABLE:
|
270
|
+
# Setup command history file
|
271
|
+
history_file = Path.home() / '.mo_diag_history'
|
272
|
+
|
273
|
+
# Create completer
|
274
|
+
completer = MODiagCompleter(self)
|
275
|
+
|
276
|
+
self.session = PromptSession(
|
277
|
+
history=FileHistory(str(history_file)),
|
278
|
+
completer=completer,
|
279
|
+
complete_while_typing=False, # Only complete on Tab
|
280
|
+
style=Style.from_dict(
|
281
|
+
{
|
282
|
+
'prompt': 'bold ansigreen',
|
283
|
+
'database': 'bold ansiyellow',
|
284
|
+
}
|
285
|
+
),
|
286
|
+
)
|
287
|
+
else:
|
288
|
+
self.session = None
|
289
|
+
|
290
|
+
if self.client and hasattr(self.client, '_connection_params'):
|
291
|
+
self.current_database = self.client._connection_params.get('database')
|
292
|
+
if self.current_database:
|
293
|
+
self.prompt = (
|
294
|
+
f'{Colors.BOLD}{Colors.GREEN}MO-DIAG{Colors.RESET}{Colors.BOLD}'
|
295
|
+
f'[{Colors.YELLOW}{self.current_database}{Colors.RESET}{Colors.BOLD}]'
|
296
|
+
f'{Colors.GREEN}>{Colors.RESET} '
|
297
|
+
)
|
298
|
+
|
299
|
+
def cmdloop(self, intro=None):
|
300
|
+
"""Override cmdloop to use prompt_toolkit for better input experience"""
|
301
|
+
if not PROMPT_TOOLKIT_AVAILABLE or not self.session:
|
302
|
+
# Fall back to standard cmdloop
|
303
|
+
return super().cmdloop(intro)
|
304
|
+
|
305
|
+
# Print intro
|
306
|
+
self.preloop()
|
307
|
+
if intro is not None:
|
308
|
+
self.intro = intro
|
309
|
+
if self.intro:
|
310
|
+
self.stdout.write(str(self.intro) + "\n")
|
311
|
+
|
312
|
+
stop = None
|
313
|
+
while not stop:
|
314
|
+
try:
|
315
|
+
# Create colored prompt
|
316
|
+
if self.current_database:
|
317
|
+
prompt_text = HTML(
|
318
|
+
f'<prompt>MO-DIAG</prompt>[<database>{self.current_database}</database>]<prompt>></prompt> '
|
319
|
+
)
|
320
|
+
else:
|
321
|
+
prompt_text = HTML('<prompt>MO-DIAG></prompt> ')
|
322
|
+
|
323
|
+
line = self.session.prompt(prompt_text)
|
324
|
+
line = self.precmd(line)
|
325
|
+
stop = self.onecmd(line)
|
326
|
+
stop = self.postcmd(stop, line)
|
327
|
+
except KeyboardInterrupt:
|
328
|
+
print("^C")
|
329
|
+
except EOFError:
|
330
|
+
print()
|
331
|
+
break
|
332
|
+
self.postloop()
|
333
|
+
|
334
|
+
def do_connect(self, arg):
|
335
|
+
"""
|
336
|
+
Connect to MatrixOne database.
|
337
|
+
|
338
|
+
Usage: connect <host> <port> <user> <password> [database]
|
339
|
+
Example: connect localhost 6001 root 111 test
|
340
|
+
"""
|
341
|
+
args = arg.split()
|
342
|
+
if len(args) < 4:
|
343
|
+
print("❌ Usage: connect <host> <port> <user> <password> [database]")
|
344
|
+
return
|
345
|
+
|
346
|
+
host = args[0]
|
347
|
+
port = int(args[1])
|
348
|
+
user = args[2]
|
349
|
+
password = args[3]
|
350
|
+
database = args[4] if len(args) > 4 else None
|
351
|
+
|
352
|
+
try:
|
353
|
+
if not self.client:
|
354
|
+
self.client = Client()
|
355
|
+
|
356
|
+
self.client.connect(host=host, port=port, user=user, password=password, database=database)
|
357
|
+
self.current_database = database
|
358
|
+
if database:
|
359
|
+
self.prompt = f'{Colors.BOLD}{Colors.GREEN}MO-DIAG{Colors.RESET}{Colors.BOLD}[{Colors.YELLOW}{database}{Colors.RESET}{Colors.BOLD}]{Colors.GREEN}>{Colors.RESET} '
|
360
|
+
else:
|
361
|
+
self.prompt = f'{Colors.BOLD}{Colors.GREEN}MO-DIAG>{Colors.RESET} '
|
362
|
+
print(
|
363
|
+
f"{Colors.GREEN}✓ Connected to {host}:{port}"
|
364
|
+
+ (f" (database: {database})" if database else "")
|
365
|
+
+ Colors.RESET
|
366
|
+
)
|
367
|
+
except Exception as e:
|
368
|
+
print(f"{Colors.RED}❌ Connection failed: {e}{Colors.RESET}")
|
369
|
+
|
370
|
+
def do_use(self, arg):
|
371
|
+
"""
|
372
|
+
Switch to a different database.
|
373
|
+
|
374
|
+
Usage: use <database>
|
375
|
+
Example: use test
|
376
|
+
"""
|
377
|
+
if not arg:
|
378
|
+
print("❌ Usage: use <database>")
|
379
|
+
return
|
380
|
+
|
381
|
+
if not self.client:
|
382
|
+
print("❌ Not connected. Use 'connect' first.")
|
383
|
+
return
|
384
|
+
|
385
|
+
# Remove trailing semicolon if present
|
386
|
+
database = arg.strip().rstrip(';')
|
387
|
+
|
388
|
+
try:
|
389
|
+
self.client.execute(f"USE {database}")
|
390
|
+
self.current_database = database
|
391
|
+
self.prompt = f'{Colors.BOLD}{Colors.GREEN}MO-DIAG{Colors.RESET}{Colors.BOLD}[{Colors.YELLOW}{database}{Colors.RESET}{Colors.BOLD}]{Colors.GREEN}>{Colors.RESET} '
|
392
|
+
print(f"{Colors.GREEN}✓ Switched to database: {database}{Colors.RESET}")
|
393
|
+
except Exception as e:
|
394
|
+
print(f"❌ Failed to switch database: {e}")
|
395
|
+
|
396
|
+
def do_show_indexes(self, arg):
|
397
|
+
"""
|
398
|
+
Show all secondary indexes for a table, including IVF, HNSW, Fulltext, and regular indexes.
|
399
|
+
|
400
|
+
Uses vertical output format (like MySQL \\G) for easy reading of long table names.
|
401
|
+
|
402
|
+
Usage: show_indexes <table_name> [database]
|
403
|
+
|
404
|
+
Example:
|
405
|
+
show_indexes cms_all_content_chunk_info
|
406
|
+
show_indexes cms_all_content_chunk_info repro3
|
407
|
+
"""
|
408
|
+
# Remove trailing semicolon
|
409
|
+
arg = arg.strip().rstrip(';')
|
410
|
+
args = arg.split()
|
411
|
+
if not args:
|
412
|
+
print("❌ Usage: show_indexes <table_name> [database]")
|
413
|
+
return
|
414
|
+
|
415
|
+
if not self.client:
|
416
|
+
print("❌ Not connected. Use 'connect' first.")
|
417
|
+
return
|
418
|
+
|
419
|
+
table_name = args[0]
|
420
|
+
database = args[1] if len(args) > 1 else self.current_database
|
421
|
+
|
422
|
+
try:
|
423
|
+
# Use the new comprehensive index detail API
|
424
|
+
indexes = self.client.get_table_indexes_detail(table_name, database)
|
425
|
+
|
426
|
+
if not indexes:
|
427
|
+
print(f"⚠️ No secondary indexes found for table '{table_name}' in database '{database}'")
|
428
|
+
return
|
429
|
+
|
430
|
+
table_info = f"'{database}.{table_name}'"
|
431
|
+
print(f"\n{header('📊 Secondary Indexes for')} {bold(table_info)}\n")
|
432
|
+
|
433
|
+
# Vertical display format (like MySQL \G)
|
434
|
+
for i, idx in enumerate(indexes, 1):
|
435
|
+
index_name = idx['index_name']
|
436
|
+
algo = idx['algo'] if idx['algo'] else 'regular'
|
437
|
+
table_type = idx['algo_table_type'] if idx['algo_table_type'] else '-'
|
438
|
+
physical_table = idx['physical_table_name']
|
439
|
+
|
440
|
+
# Filter out internal columns like __mo_alias___mo_cpkey_col
|
441
|
+
user_columns = [col for col in idx['columns'] if not col.startswith('__mo_alias_')]
|
442
|
+
columns = ', '.join(user_columns) if user_columns else 'N/A'
|
443
|
+
|
444
|
+
# Color code by algorithm type
|
445
|
+
if algo == 'ivfflat':
|
446
|
+
algo_display = f"{Colors.CYAN}{algo}{Colors.RESET}"
|
447
|
+
elif algo == 'hnsw':
|
448
|
+
algo_display = f"{Colors.GREEN}{algo}{Colors.RESET}"
|
449
|
+
elif algo == 'fulltext':
|
450
|
+
algo_display = f"{Colors.YELLOW}{algo}{Colors.RESET}"
|
451
|
+
else:
|
452
|
+
algo_display = algo
|
453
|
+
|
454
|
+
# Print in vertical format
|
455
|
+
print(info(f"{'*' * 27} {i}. row {'*' * 27}"))
|
456
|
+
print(f" {bold('Index Name')}: {Colors.CYAN}{index_name}{Colors.RESET}")
|
457
|
+
print(f" {bold('Algorithm')}: {algo_display}")
|
458
|
+
print(f" {bold('Table Type')}: {table_type}")
|
459
|
+
print(f" {bold('Physical Table')}: {physical_table}")
|
460
|
+
print(f" {bold('Columns')}: {columns}")
|
461
|
+
|
462
|
+
# For vector/fulltext indexes, show table statistics using metadata.scan interface
|
463
|
+
if algo in ['ivfflat', 'hnsw', 'fulltext']:
|
464
|
+
try:
|
465
|
+
# Use SDK's metadata.scan interface with columns="*" to get structured results
|
466
|
+
stats = self.client.metadata.scan(database, physical_table, columns="*")
|
467
|
+
|
468
|
+
if stats:
|
469
|
+
# Aggregate statistics from all objects
|
470
|
+
total_rows = 0
|
471
|
+
total_compress_size = 0
|
472
|
+
total_origin_size = 0
|
473
|
+
|
474
|
+
# Deduplicate by object_name (same logic as show_table_stats)
|
475
|
+
seen_objects = set()
|
476
|
+
object_count = 0
|
477
|
+
|
478
|
+
for obj in stats:
|
479
|
+
# MetadataRow has attributes, not dictionary keys
|
480
|
+
object_name = getattr(obj, 'object_name', None)
|
481
|
+
if object_name and object_name not in seen_objects:
|
482
|
+
seen_objects.add(object_name)
|
483
|
+
object_count += 1
|
484
|
+
|
485
|
+
# Sum up statistics using attributes
|
486
|
+
row_cnt = getattr(obj, 'row_cnt', None) or getattr(obj, 'rows_cnt', 0) or 0
|
487
|
+
total_rows += int(row_cnt) if row_cnt else 0
|
488
|
+
|
489
|
+
compress_size = getattr(obj, 'compress_size', 0) or 0
|
490
|
+
total_compress_size += int(compress_size) if compress_size else 0
|
491
|
+
|
492
|
+
origin_size = getattr(obj, 'origin_size', 0) or 0
|
493
|
+
total_origin_size += int(origin_size) if origin_size else 0
|
494
|
+
|
495
|
+
# Format sizes
|
496
|
+
def format_size(size_bytes):
|
497
|
+
if size_bytes >= 1024 * 1024:
|
498
|
+
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
499
|
+
elif size_bytes >= 1024:
|
500
|
+
return f"{size_bytes / 1024:.2f} KB"
|
501
|
+
else:
|
502
|
+
return f"{size_bytes} B"
|
503
|
+
|
504
|
+
compress_size_str = format_size(total_compress_size)
|
505
|
+
origin_size_str = format_size(total_origin_size)
|
506
|
+
|
507
|
+
print(f" {bold('Statistics')}:")
|
508
|
+
print(f" - Objects: {object_count}")
|
509
|
+
print(f" - Rows: {total_rows:,}")
|
510
|
+
print(f" - Compressed Size: {compress_size_str}")
|
511
|
+
print(f" - Original Size: {origin_size_str}")
|
512
|
+
except Exception:
|
513
|
+
# If stats not available, just skip (no error message needed)
|
514
|
+
pass
|
515
|
+
|
516
|
+
# Summary
|
517
|
+
print(info("=" * 60))
|
518
|
+
|
519
|
+
algo_counts = {}
|
520
|
+
for idx in indexes:
|
521
|
+
algo = idx['algo'] if idx['algo'] else 'regular'
|
522
|
+
algo_counts[algo] = algo_counts.get(algo, 0) + 1
|
523
|
+
|
524
|
+
summary_parts = []
|
525
|
+
for algo, count in sorted(algo_counts.items()):
|
526
|
+
summary_parts.append(f"{count} {algo}")
|
527
|
+
|
528
|
+
print(bold(f"Total: {len(indexes)} index tables") + f" ({', '.join(summary_parts)})" + "\n")
|
529
|
+
|
530
|
+
except Exception as e:
|
531
|
+
print(f"❌ Error: {e}")
|
532
|
+
|
533
|
+
def do_show_all_indexes(self, arg):
|
534
|
+
"""
|
535
|
+
Show index health report for all tables with secondary indexes.
|
536
|
+
|
537
|
+
This command performs diagnostic checks including:
|
538
|
+
- Row count consistency between main table and index tables
|
539
|
+
- Vector index building status (IVF/HNSW)
|
540
|
+
- Index type distribution
|
541
|
+
- Problem detection
|
542
|
+
|
543
|
+
Usage: show_all_indexes [database]
|
544
|
+
Example:
|
545
|
+
show_all_indexes
|
546
|
+
show_all_indexes repro3
|
547
|
+
"""
|
548
|
+
# Remove trailing semicolon
|
549
|
+
arg = arg.strip().rstrip(';')
|
550
|
+
database = arg if arg else self.current_database
|
551
|
+
|
552
|
+
if not database:
|
553
|
+
print("❌ No database specified and no current database set. Use 'use <database>' or provide database name.")
|
554
|
+
return
|
555
|
+
|
556
|
+
if not self.client:
|
557
|
+
print("❌ Not connected. Use 'connect' first.")
|
558
|
+
return
|
559
|
+
|
560
|
+
try:
|
561
|
+
# Get all tables with indexes
|
562
|
+
sql = """
|
563
|
+
SELECT DISTINCT mo_tables.relname AS table_name
|
564
|
+
FROM mo_catalog.mo_indexes
|
565
|
+
JOIN mo_catalog.mo_tables ON mo_indexes.table_id = mo_tables.rel_id
|
566
|
+
WHERE mo_indexes.type IN ('MULTIPLE', 'UNIQUE') AND mo_tables.reldatabase = ?
|
567
|
+
ORDER BY mo_tables.relname
|
568
|
+
"""
|
569
|
+
result = self.client.execute(sql, (database,))
|
570
|
+
tables = [row[0] for row in result.fetchall()]
|
571
|
+
|
572
|
+
if not tables:
|
573
|
+
print(f"⚠️ No tables with secondary indexes found in database '{database}'")
|
574
|
+
return
|
575
|
+
|
576
|
+
print(f"\n{header('📊 Index Health Report')} for Database '{database}':")
|
577
|
+
print("=" * 120)
|
578
|
+
|
579
|
+
healthy_tables = []
|
580
|
+
attention_tables = []
|
581
|
+
|
582
|
+
# Check each table
|
583
|
+
for table_name in tables:
|
584
|
+
try:
|
585
|
+
# Get index details
|
586
|
+
indexes = self.client.get_table_indexes_detail(table_name, database)
|
587
|
+
index_count = len(set(idx['index_name'] for idx in indexes))
|
588
|
+
|
589
|
+
# Determine if table has special indexes (IVF/HNSW/Fulltext)
|
590
|
+
has_vector_or_fulltext = any(idx['algo'] in ['ivfflat', 'hnsw', 'fulltext'] for idx in indexes)
|
591
|
+
|
592
|
+
# Check row consistency (only for regular/UNIQUE indexes)
|
593
|
+
consistency_status = None
|
594
|
+
has_issue = False
|
595
|
+
issue_detail = None
|
596
|
+
|
597
|
+
if not has_vector_or_fulltext:
|
598
|
+
# Only check row consistency for tables with regular/UNIQUE indexes
|
599
|
+
try:
|
600
|
+
row_count = self.client.verify_table_index_counts(table_name)
|
601
|
+
consistency_status = f"✓ {row_count:,} rows"
|
602
|
+
except ValueError as e:
|
603
|
+
consistency_status = "❌ Mismatch"
|
604
|
+
has_issue = True
|
605
|
+
issue_detail = str(e).split('\n')[0] # First line of error
|
606
|
+
except Exception:
|
607
|
+
consistency_status = "⚠️ Unknown"
|
608
|
+
else:
|
609
|
+
# For vector/fulltext indexes, just show row count without verification
|
610
|
+
try:
|
611
|
+
result = self.client.execute(f"SELECT COUNT(*) FROM `{table_name}`")
|
612
|
+
row_count = result.fetchone()[0]
|
613
|
+
consistency_status = f"{row_count:,} rows"
|
614
|
+
except Exception:
|
615
|
+
consistency_status = "Unknown"
|
616
|
+
|
617
|
+
# Check IVF/HNSW/Fulltext status
|
618
|
+
special_index_status = None
|
619
|
+
for idx in indexes:
|
620
|
+
if idx['algo'] == 'ivfflat':
|
621
|
+
try:
|
622
|
+
stats = self.client.vector_ops.get_ivf_stats(table_name, idx['columns'][0])
|
623
|
+
if stats and 'distribution' in stats:
|
624
|
+
centroid_ids = stats['distribution'].get('centroid_id', [])
|
625
|
+
centroid_counts = stats['distribution'].get('centroid_count', [])
|
626
|
+
centroid_count = len(centroid_ids)
|
627
|
+
total_vectors = sum(centroid_counts) if centroid_counts else 0
|
628
|
+
|
629
|
+
if centroid_count > 0 and total_vectors > 0:
|
630
|
+
special_index_status = f"IVF: {centroid_count} centroids, {total_vectors} vectors"
|
631
|
+
elif centroid_count > 0:
|
632
|
+
special_index_status = f"IVF: {centroid_count} centroids"
|
633
|
+
else:
|
634
|
+
special_index_status = "IVF: building"
|
635
|
+
has_issue = True
|
636
|
+
issue_detail = "IVF index not built yet"
|
637
|
+
else:
|
638
|
+
special_index_status = "IVF: no stats available"
|
639
|
+
except Exception as e:
|
640
|
+
error_msg = str(e)
|
641
|
+
# Truncate long error messages for display
|
642
|
+
if len(error_msg) > 30:
|
643
|
+
error_short = error_msg[:27] + "..."
|
644
|
+
else:
|
645
|
+
error_short = error_msg
|
646
|
+
special_index_status = f"IVF: error ({error_short})"
|
647
|
+
has_issue = True
|
648
|
+
if not issue_detail:
|
649
|
+
issue_detail = f"Failed to get IVF stats: {error_msg}"
|
650
|
+
elif idx['algo'] == 'hnsw':
|
651
|
+
special_index_status = "HNSW index"
|
652
|
+
elif idx['algo'] == 'fulltext':
|
653
|
+
special_index_status = "Fulltext index"
|
654
|
+
|
655
|
+
# Categorize table
|
656
|
+
table_info = {
|
657
|
+
'name': table_name,
|
658
|
+
'index_count': index_count,
|
659
|
+
'consistency': consistency_status,
|
660
|
+
'special_index_status': special_index_status,
|
661
|
+
'has_issue': has_issue,
|
662
|
+
'issue_detail': issue_detail,
|
663
|
+
}
|
664
|
+
|
665
|
+
if has_issue:
|
666
|
+
attention_tables.append(table_info)
|
667
|
+
else:
|
668
|
+
healthy_tables.append(table_info)
|
669
|
+
|
670
|
+
except Exception as e:
|
671
|
+
# If we can't check this table, mark it as needing attention
|
672
|
+
attention_tables.append(
|
673
|
+
{
|
674
|
+
'name': table_name,
|
675
|
+
'index_count': '?',
|
676
|
+
'consistency': '❌ Error',
|
677
|
+
'special_index_status': None,
|
678
|
+
'has_issue': True,
|
679
|
+
'issue_detail': str(e)[:50],
|
680
|
+
}
|
681
|
+
)
|
682
|
+
|
683
|
+
# Display healthy tables
|
684
|
+
if healthy_tables:
|
685
|
+
print(f"\n{success('✓ HEALTHY')} ({len(healthy_tables)} tables)")
|
686
|
+
print("-" * 120)
|
687
|
+
print(f"{'Table Name':<35} | {'Indexes':<8} | {'Row Count':<20} | {'Notes'}")
|
688
|
+
print("-" * 120)
|
689
|
+
|
690
|
+
for table in healthy_tables:
|
691
|
+
notes = table['special_index_status'] if table['special_index_status'] else '-'
|
692
|
+
print(f"{table['name']:<35} | {table['index_count']:<8} | {table['consistency']:<20} | {notes}")
|
693
|
+
|
694
|
+
# Display tables needing attention
|
695
|
+
if attention_tables:
|
696
|
+
print(f"\n{warning('⚠️ ATTENTION NEEDED')} ({len(attention_tables)} tables)")
|
697
|
+
print("-" * 120)
|
698
|
+
print(f"{'Table Name':<35} | {'Issue':<40} | {'Details'}")
|
699
|
+
print("-" * 120)
|
700
|
+
|
701
|
+
for table in attention_tables:
|
702
|
+
if 'Mismatch' in (table['consistency'] or ''):
|
703
|
+
issue = "Row count mismatch between indexes"
|
704
|
+
details = table['issue_detail'] if table['issue_detail'] else "Check with verify_counts"
|
705
|
+
elif 'building' in (table.get('special_index_status') or ''):
|
706
|
+
issue = "Vector index building incomplete"
|
707
|
+
details = table['issue_detail'] if table['issue_detail'] else "Check with show_ivf_status"
|
708
|
+
elif 'error' in (table.get('special_index_status') or '').lower():
|
709
|
+
issue = "Vector index error"
|
710
|
+
details = table['issue_detail'] if table['issue_detail'] else "Check index status"
|
711
|
+
elif 'Error' in (table['consistency'] or ''):
|
712
|
+
issue = "Unable to verify table"
|
713
|
+
details = table['issue_detail'] if table['issue_detail'] else "Unknown error"
|
714
|
+
else:
|
715
|
+
issue = "Unknown issue"
|
716
|
+
details = str(table.get('issue_detail', '-'))
|
717
|
+
|
718
|
+
print(f"{table['name']:<35} | {issue:<40} | {details[:38]}")
|
719
|
+
|
720
|
+
# Summary
|
721
|
+
print("\n" + "=" * 120)
|
722
|
+
print(f"{bold('Summary:')}")
|
723
|
+
print(f" {success('✓')} {len(healthy_tables)} healthy tables")
|
724
|
+
if attention_tables:
|
725
|
+
print(f" {warning('⚠️ ')} {len(attention_tables)} tables need attention")
|
726
|
+
else:
|
727
|
+
print(f" {success('✓')} All indexes healthy!")
|
728
|
+
print(f" Total: {len(tables)} tables with indexes\n")
|
729
|
+
|
730
|
+
if attention_tables:
|
731
|
+
print(f"{info('💡 Tip:')} Use 'verify_counts <table>' or 'show_ivf_status' for detailed diagnostics")
|
732
|
+
|
733
|
+
except Exception as e:
|
734
|
+
print(f"❌ Error: {e}")
|
735
|
+
|
736
|
+
def do_verify_counts(self, arg):
|
737
|
+
"""
|
738
|
+
Verify row counts between main table and all its index tables.
|
739
|
+
|
740
|
+
Usage: verify_counts <table_name> [database]
|
741
|
+
Example:
|
742
|
+
verify_counts cms_all_content_chunk_info
|
743
|
+
verify_counts cms_all_content_chunk_info repro3
|
744
|
+
"""
|
745
|
+
# Remove trailing semicolon
|
746
|
+
arg = arg.strip().rstrip(';')
|
747
|
+
args = arg.split()
|
748
|
+
if not args:
|
749
|
+
print("❌ Usage: verify_counts <table_name> [database]")
|
750
|
+
return
|
751
|
+
|
752
|
+
if not self.client:
|
753
|
+
print("❌ Not connected. Use 'connect' first.")
|
754
|
+
return
|
755
|
+
|
756
|
+
table_name = args[0]
|
757
|
+
database = args[1] if len(args) > 1 else self.current_database
|
758
|
+
|
759
|
+
if not database:
|
760
|
+
print("❌ No database specified and no current database set.")
|
761
|
+
return
|
762
|
+
|
763
|
+
try:
|
764
|
+
# Switch to the database temporarily if needed
|
765
|
+
current_db = self.client._connection_params.get('database')
|
766
|
+
if current_db != database:
|
767
|
+
self.client.execute(f"USE {database}")
|
768
|
+
|
769
|
+
# Get index tables
|
770
|
+
index_tables = self.client.get_secondary_index_tables(table_name, database)
|
771
|
+
|
772
|
+
if not index_tables:
|
773
|
+
print(f"⚠️ No secondary indexes found for table '{table_name}'")
|
774
|
+
# Still show main table count
|
775
|
+
result = self.client.execute(f"SELECT COUNT(*) FROM `{table_name}`")
|
776
|
+
count = result.fetchone()[0]
|
777
|
+
print(f"Main table '{table_name}': {count:,} rows")
|
778
|
+
return
|
779
|
+
|
780
|
+
# Build verification SQL
|
781
|
+
from .index_utils import build_verify_counts_sql
|
782
|
+
|
783
|
+
sql = build_verify_counts_sql(table_name, index_tables)
|
784
|
+
|
785
|
+
result = self.client.execute(sql)
|
786
|
+
row = result.fetchone()
|
787
|
+
|
788
|
+
main_count = row[0]
|
789
|
+
mismatches = []
|
790
|
+
|
791
|
+
table_info = f"'{database}.{table_name}'"
|
792
|
+
print(f"\n{header('📊 Row Count Verification for')} {bold(table_info)}")
|
793
|
+
print(info("=" * 80))
|
794
|
+
print(bold(f"Main table: {main_count:,} rows"))
|
795
|
+
print(info("-" * 80))
|
796
|
+
|
797
|
+
for idx, index_table in enumerate(index_tables):
|
798
|
+
index_count = row[idx + 1]
|
799
|
+
if index_count == main_count:
|
800
|
+
print(f"{success('✓')} {index_table}: {bold(f'{index_count:,}')} rows")
|
801
|
+
else:
|
802
|
+
print(f"{error('✗ MISMATCH')} {index_table}: {bold(f'{index_count:,}')} rows")
|
803
|
+
mismatches.append((index_table, index_count))
|
804
|
+
|
805
|
+
print(info("=" * 80))
|
806
|
+
|
807
|
+
if mismatches:
|
808
|
+
print(f"❌ FAILED: {len(mismatches)} index table(s) have mismatched counts!")
|
809
|
+
else:
|
810
|
+
print(f"✅ PASSED: All index tables match ({main_count:,} rows)")
|
811
|
+
print()
|
812
|
+
|
813
|
+
# Switch back if needed
|
814
|
+
if current_db and current_db != database:
|
815
|
+
self.client.execute(f"USE {current_db}")
|
816
|
+
|
817
|
+
except Exception as e:
|
818
|
+
print(f"❌ Error: {e}")
|
819
|
+
|
820
|
+
def do_show_ivf_status(self, arg):
|
821
|
+
"""
|
822
|
+
Show IVF index centroids building status.
|
823
|
+
|
824
|
+
Usage:
|
825
|
+
show_ivf_status [database] - Show compact summary
|
826
|
+
show_ivf_status [database] -v - Show detailed view
|
827
|
+
show_ivf_status [database] -t table - Filter by table name
|
828
|
+
|
829
|
+
Example:
|
830
|
+
show_ivf_status
|
831
|
+
show_ivf_status test -v
|
832
|
+
show_ivf_status test -t my_table
|
833
|
+
"""
|
834
|
+
# Parse arguments
|
835
|
+
args = arg.strip().rstrip(';').split() if arg.strip() else []
|
836
|
+
database = None
|
837
|
+
verbose = False
|
838
|
+
filter_table = None
|
839
|
+
|
840
|
+
i = 0
|
841
|
+
while i < len(args):
|
842
|
+
if args[i] == '-v':
|
843
|
+
verbose = True
|
844
|
+
elif args[i] == '-t' and i + 1 < len(args):
|
845
|
+
filter_table = args[i + 1]
|
846
|
+
i += 1
|
847
|
+
elif not database:
|
848
|
+
database = args[i]
|
849
|
+
i += 1
|
850
|
+
|
851
|
+
if not database:
|
852
|
+
database = self.current_database
|
853
|
+
|
854
|
+
if not database:
|
855
|
+
print("❌ No database specified and no current database set.")
|
856
|
+
return
|
857
|
+
|
858
|
+
if not self.client:
|
859
|
+
print("❌ Not connected. Use 'connect' first.")
|
860
|
+
return
|
861
|
+
|
862
|
+
try:
|
863
|
+
# Get all tables with IVF indexes and their column information
|
864
|
+
# Group by table and index name to avoid duplicates from multiple index table types
|
865
|
+
sql = """
|
866
|
+
SELECT
|
867
|
+
mo_tables.relname,
|
868
|
+
mo_indexes.name,
|
869
|
+
mo_indexes.column_name,
|
870
|
+
MAX(mo_indexes.algo_table_type) as algo_table_type
|
871
|
+
FROM mo_catalog.mo_indexes
|
872
|
+
JOIN mo_catalog.mo_tables ON mo_indexes.table_id = mo_tables.rel_id
|
873
|
+
WHERE mo_tables.reldatabase = ?
|
874
|
+
AND mo_indexes.algo LIKE '%ivf%'
|
875
|
+
GROUP BY mo_tables.relname, mo_indexes.name, mo_indexes.column_name
|
876
|
+
ORDER BY mo_tables.relname, mo_indexes.name
|
877
|
+
"""
|
878
|
+
result = self.client.execute(sql, (database,))
|
879
|
+
index_info = result.fetchall()
|
880
|
+
|
881
|
+
if not index_info:
|
882
|
+
print(f"⚠️ No IVF indexes found in database '{database}'")
|
883
|
+
return
|
884
|
+
|
885
|
+
# Filter by table if specified
|
886
|
+
if filter_table:
|
887
|
+
index_info = [row for row in index_info if row[0] == filter_table]
|
888
|
+
if not index_info:
|
889
|
+
print(f"⚠️ No IVF indexes found for table '{filter_table}'")
|
890
|
+
return
|
891
|
+
|
892
|
+
print(f"\n📊 IVF Index Status in '{database}':")
|
893
|
+
print("=" * 150)
|
894
|
+
|
895
|
+
if not verbose:
|
896
|
+
# Compact table view
|
897
|
+
print(
|
898
|
+
f"{'Table':<30} | {'Index':<25} | {'Column':<20} | {'Centroids':<10} | {'Vectors':<12} | {'Balance':<10} | {'Status':<15}"
|
899
|
+
)
|
900
|
+
print("-" * 150)
|
901
|
+
|
902
|
+
total_ivf_indexes = 0
|
903
|
+
|
904
|
+
# For each index, get its stats using SDK API
|
905
|
+
for row in index_info:
|
906
|
+
table_name = row[0]
|
907
|
+
index_name = row[1]
|
908
|
+
column_name = row[2]
|
909
|
+
algo_table_type = row[3] if len(row) > 3 else None
|
910
|
+
|
911
|
+
try:
|
912
|
+
# Use SDK's get_ivf_stats method with column name
|
913
|
+
stats = self.client.vector_ops.get_ivf_stats(table_name, column_name)
|
914
|
+
|
915
|
+
if stats and 'index_tables' in stats:
|
916
|
+
# Show centroid distribution
|
917
|
+
if 'distribution' in stats:
|
918
|
+
dist = stats['distribution']
|
919
|
+
centroid_counts = dist.get('centroid_count', [])
|
920
|
+
|
921
|
+
if centroid_counts:
|
922
|
+
total_centroids = len(centroid_counts)
|
923
|
+
total_vectors = sum(centroid_counts)
|
924
|
+
min_count = min(centroid_counts)
|
925
|
+
max_count = max(centroid_counts)
|
926
|
+
avg_count = total_vectors / total_centroids if total_centroids > 0 else 0
|
927
|
+
balance = max_count / min_count if min_count > 0 else 0
|
928
|
+
|
929
|
+
if verbose:
|
930
|
+
print(f"\nTable: {table_name} | Index: {index_name} | Column: {column_name}")
|
931
|
+
print("-" * 150)
|
932
|
+
|
933
|
+
# Show physical tables
|
934
|
+
index_tables = stats['index_tables']
|
935
|
+
print("Physical Tables:")
|
936
|
+
for table_type, physical_table in index_tables.items():
|
937
|
+
print(f" - {table_type:<15}: {physical_table}")
|
938
|
+
|
939
|
+
print("\nCentroid Distribution:")
|
940
|
+
print(f" Total Centroids: {total_centroids}")
|
941
|
+
print(f" Total Vectors: {total_vectors:,}")
|
942
|
+
print(f" Min/Avg/Max: {min_count} / {avg_count:.1f} / {max_count}")
|
943
|
+
print(
|
944
|
+
f" Load Balance: {balance:.2f}x" if min_count > 0 else " Load Balance: N/A"
|
945
|
+
)
|
946
|
+
|
947
|
+
# Show top 10 centroids by size
|
948
|
+
if len(centroid_counts) > 0:
|
949
|
+
centroid_ids = dist.get('centroid_id', [])
|
950
|
+
centroid_versions = dist.get('centroid_version', [])
|
951
|
+
print("\n Top Centroids (by vector count):")
|
952
|
+
centroid_data = list(zip(centroid_ids, centroid_counts, centroid_versions))
|
953
|
+
centroid_data.sort(key=lambda x: x[1], reverse=True)
|
954
|
+
|
955
|
+
for i, (cid, count, version) in enumerate(centroid_data[:10], 1):
|
956
|
+
print(f" {i:2}. Centroid {cid}: {count:,} vectors (version {version})")
|
957
|
+
else:
|
958
|
+
# Compact view
|
959
|
+
status = "✓ active"
|
960
|
+
print(
|
961
|
+
f"{table_name:<30} | {index_name:<25} | {column_name:<20} | {total_centroids:<10} | {total_vectors:<12,} | {balance:<10.2f} | {status:<15}"
|
962
|
+
)
|
963
|
+
|
964
|
+
total_ivf_indexes += 1
|
965
|
+
else:
|
966
|
+
if verbose:
|
967
|
+
print("\nCentroid Distribution: ⚠️ No centroids found (empty index)")
|
968
|
+
else:
|
969
|
+
print(
|
970
|
+
f"{table_name:<30} | {index_name:<25} | {column_name:<20} | {'0':<10} | {'0':<12} | {'N/A':<10} | {'⚠️ empty':<15}"
|
971
|
+
)
|
972
|
+
else:
|
973
|
+
if verbose:
|
974
|
+
print("\nCentroid Distribution: ⚠️ No distribution data available")
|
975
|
+
else:
|
976
|
+
print(
|
977
|
+
f"{table_name:<30} | {index_name:<25} | {column_name:<20} | {'?':<10} | {'?':<12} | {'?':<10} | {'⚠️ no data':<15}"
|
978
|
+
)
|
979
|
+
else:
|
980
|
+
# Index exists but no stats available
|
981
|
+
if verbose:
|
982
|
+
print(f"\nTable: {table_name} | Index: {index_name}")
|
983
|
+
print("-" * 150)
|
984
|
+
print(f"Status: ⚠️ {algo_table_type if algo_table_type else 'No stats available'}")
|
985
|
+
else:
|
986
|
+
print(
|
987
|
+
f"{table_name:<30} | {index_name:<25} | {column_name:<20} | {'?':<10} | {'?':<12} | {'?':<10} | {'⚠️ no stats':<15}"
|
988
|
+
)
|
989
|
+
|
990
|
+
except Exception as e:
|
991
|
+
# If we can't get IVF stats, show the error
|
992
|
+
if verbose:
|
993
|
+
print(f"\nTable: {table_name} | Index: {index_name}")
|
994
|
+
print("-" * 150)
|
995
|
+
print(f"Status: ❌ Error - {str(e)}")
|
996
|
+
else:
|
997
|
+
error_msg = str(e)[:10]
|
998
|
+
print(
|
999
|
+
f"{table_name:<30} | {index_name:<25} | {column_name:<20} | {'?':<10} | {'?':<12} | {'?':<10} | {'❌ ' + error_msg:<15}"
|
1000
|
+
)
|
1001
|
+
continue
|
1002
|
+
|
1003
|
+
print("=" * 150)
|
1004
|
+
if total_ivf_indexes == 0:
|
1005
|
+
print("⚠️ No accessible IVF indexes found")
|
1006
|
+
else:
|
1007
|
+
print(f"Total: {total_ivf_indexes} IVF indexes")
|
1008
|
+
if not verbose:
|
1009
|
+
print(f"\nTip: Use 'show_ivf_status {database} -v' for detailed view with top centroids")
|
1010
|
+
print(f" Use 'show_ivf_status {database} -t <table>' to filter by table")
|
1011
|
+
print()
|
1012
|
+
|
1013
|
+
except Exception as e:
|
1014
|
+
print(f"❌ Error: {e}")
|
1015
|
+
|
1016
|
+
def do_show_table_stats(self, arg):
|
1017
|
+
"""
|
1018
|
+
Show table statistics using metadata_scan.
|
1019
|
+
|
1020
|
+
Usage:
|
1021
|
+
show_table_stats <table> [database] - Show table stats summary
|
1022
|
+
show_table_stats <table> [database] -t - Include tombstone stats
|
1023
|
+
show_table_stats <table> [database] -i idx1,idx2 - Include specific index stats
|
1024
|
+
show_table_stats <table> [database] -a - Include all (tombstone + all indexes)
|
1025
|
+
show_table_stats <table> [database] -d - Show detailed object list
|
1026
|
+
|
1027
|
+
Example:
|
1028
|
+
show_table_stats my_table
|
1029
|
+
show_table_stats my_table test -t
|
1030
|
+
show_table_stats my_table test -i idx_vec,idx_name
|
1031
|
+
show_table_stats my_table test -a
|
1032
|
+
show_table_stats my_table test -a -d
|
1033
|
+
"""
|
1034
|
+
args = arg.strip().rstrip(';').split() if arg.strip() else []
|
1035
|
+
|
1036
|
+
if not args:
|
1037
|
+
print("❌ Usage: show_table_stats <table> [database] [-t] [-i index_names] [-a] [-d]")
|
1038
|
+
return
|
1039
|
+
|
1040
|
+
table_name = args[0]
|
1041
|
+
database = None
|
1042
|
+
include_tombstone = False
|
1043
|
+
include_indexes = None
|
1044
|
+
include_all = False
|
1045
|
+
show_detail = False
|
1046
|
+
|
1047
|
+
# Parse remaining arguments
|
1048
|
+
i = 1
|
1049
|
+
while i < len(args):
|
1050
|
+
if args[i] == '-t':
|
1051
|
+
include_tombstone = True
|
1052
|
+
elif args[i] == '-a':
|
1053
|
+
include_all = True
|
1054
|
+
elif args[i] == '-d':
|
1055
|
+
show_detail = True
|
1056
|
+
elif args[i] == '-i' and i + 1 < len(args):
|
1057
|
+
include_indexes = [idx.strip() for idx in args[i + 1].split(',')]
|
1058
|
+
i += 1
|
1059
|
+
elif not database:
|
1060
|
+
database = args[i]
|
1061
|
+
i += 1
|
1062
|
+
|
1063
|
+
if not database:
|
1064
|
+
database = self.current_database
|
1065
|
+
|
1066
|
+
if not database:
|
1067
|
+
print("❌ No database specified and no current database set.")
|
1068
|
+
return
|
1069
|
+
|
1070
|
+
if not self.client:
|
1071
|
+
print("❌ Not connected. Use 'connect' first.")
|
1072
|
+
return
|
1073
|
+
|
1074
|
+
try:
|
1075
|
+
# If -a flag is set, get all secondary indexes for the table
|
1076
|
+
if include_all:
|
1077
|
+
include_tombstone = True
|
1078
|
+
# Get all secondary indexes for the table (exclude PRIMARY and UNIQUE)
|
1079
|
+
try:
|
1080
|
+
index_list = self.client.get_secondary_index_tables(table_name, database)
|
1081
|
+
if index_list:
|
1082
|
+
# Extract index names from physical table names
|
1083
|
+
# Physical table names are like: __mo_index_secondary_xxxxx
|
1084
|
+
# We need to get the actual index names from mo_indexes
|
1085
|
+
# Filter out PRIMARY, UNIQUE and other system indexes
|
1086
|
+
sql = """
|
1087
|
+
SELECT DISTINCT mo_indexes.name
|
1088
|
+
FROM mo_catalog.mo_indexes
|
1089
|
+
JOIN mo_catalog.mo_tables ON mo_indexes.table_id = mo_tables.rel_id
|
1090
|
+
WHERE mo_tables.relname = ?
|
1091
|
+
AND mo_tables.reldatabase = ?
|
1092
|
+
AND mo_indexes.name NOT IN ('PRIMARY', '__mo_rowid_idx')
|
1093
|
+
AND mo_indexes.type != 'PRIMARY KEY'
|
1094
|
+
AND mo_indexes.type != 'UNIQUE'
|
1095
|
+
"""
|
1096
|
+
result = self.client.execute(sql, (table_name, database))
|
1097
|
+
include_indexes = [row[0] for row in result.fetchall()]
|
1098
|
+
if not include_indexes:
|
1099
|
+
include_indexes = None # No secondary indexes
|
1100
|
+
except Exception as e:
|
1101
|
+
print(f"⚠️ Warning: Could not fetch index list: {e}")
|
1102
|
+
include_indexes = None
|
1103
|
+
|
1104
|
+
if show_detail:
|
1105
|
+
# Check if showing all indexes (-a -d together = hierarchical view)
|
1106
|
+
if include_all:
|
1107
|
+
# Show hierarchical view: Table -> Index Name -> Physical Table (with type) -> Object List
|
1108
|
+
print(f"\n📊 Detailed Table Statistics for '{database}.{table_name}':")
|
1109
|
+
print("=" * 150)
|
1110
|
+
|
1111
|
+
# Helper functions
|
1112
|
+
def format_size(size_bytes):
|
1113
|
+
if isinstance(size_bytes, str):
|
1114
|
+
return size_bytes
|
1115
|
+
if size_bytes >= 1024 * 1024:
|
1116
|
+
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
1117
|
+
elif size_bytes >= 1024:
|
1118
|
+
return f"{size_bytes / 1024:.2f} KB"
|
1119
|
+
else:
|
1120
|
+
return f"{size_bytes} B"
|
1121
|
+
|
1122
|
+
def deduplicate_objects(objects):
|
1123
|
+
seen = set()
|
1124
|
+
unique_objects = []
|
1125
|
+
for obj in objects:
|
1126
|
+
# Handle both dict and MetadataRow objects
|
1127
|
+
if hasattr(obj, 'object_name'):
|
1128
|
+
obj_name = getattr(obj, 'object_name', None)
|
1129
|
+
elif isinstance(obj, dict):
|
1130
|
+
obj_name = obj.get('object_name')
|
1131
|
+
else:
|
1132
|
+
obj_name = None
|
1133
|
+
if obj_name and obj_name not in seen:
|
1134
|
+
seen.add(obj_name)
|
1135
|
+
unique_objects.append(obj)
|
1136
|
+
return unique_objects
|
1137
|
+
|
1138
|
+
# 1. Show main table with aggregated stats
|
1139
|
+
try:
|
1140
|
+
main_table_objects = self.client.metadata.scan(database, table_name, columns="*")
|
1141
|
+
if main_table_objects:
|
1142
|
+
unique_objs = deduplicate_objects(list(main_table_objects))
|
1143
|
+
total_rows = sum(getattr(obj, 'row_cnt', 0) or 0 for obj in unique_objs)
|
1144
|
+
total_null = sum(getattr(obj, 'null_cnt', 0) or 0 for obj in unique_objs)
|
1145
|
+
total_origin = sum(getattr(obj, 'origin_size', 0) or 0 for obj in unique_objs)
|
1146
|
+
total_compress = sum(getattr(obj, 'compress_size', 0) or 0 for obj in unique_objs)
|
1147
|
+
|
1148
|
+
print(f"\n{bold('Table:')} {Colors.CYAN}{table_name}{Colors.RESET}")
|
1149
|
+
print(
|
1150
|
+
f" Objects: {len(unique_objs)} | Rows: {total_rows:,} | Null: {total_null:,} | Original: {format_size(total_origin)} | Compressed: {format_size(total_compress)}"
|
1151
|
+
)
|
1152
|
+
|
1153
|
+
# Show object details
|
1154
|
+
if unique_objs:
|
1155
|
+
print(f"\n {bold('Objects:')}")
|
1156
|
+
print(
|
1157
|
+
f" {'Object Name':<50} | {'Rows':<12} | {'Null Cnt':<10} | {'Original Size':<15} | {'Compressed Size':<15}"
|
1158
|
+
)
|
1159
|
+
print(" " + "-" * 148)
|
1160
|
+
for obj in unique_objs:
|
1161
|
+
obj_name = getattr(obj, 'object_name', 'N/A')
|
1162
|
+
rows = getattr(obj, 'row_cnt', 0) or 0
|
1163
|
+
nulls = getattr(obj, 'null_cnt', 0) or 0
|
1164
|
+
orig_size = getattr(obj, 'origin_size', 0) or 0
|
1165
|
+
comp_size = getattr(obj, 'compress_size', 0) or 0
|
1166
|
+
print(
|
1167
|
+
f" {obj_name:<50} | {rows:<12,} | {nulls:<10,} | {format_size(orig_size):<15} | {format_size(comp_size):<15}"
|
1168
|
+
)
|
1169
|
+
except Exception:
|
1170
|
+
pass
|
1171
|
+
|
1172
|
+
# 2. Get all indexes and their physical tables
|
1173
|
+
try:
|
1174
|
+
indexes = self.client.get_table_indexes_detail(table_name, database)
|
1175
|
+
|
1176
|
+
if indexes:
|
1177
|
+
# Group indexes by index name
|
1178
|
+
indexes_by_name = {}
|
1179
|
+
for idx in indexes:
|
1180
|
+
idx_name = idx['index_name']
|
1181
|
+
if idx_name not in indexes_by_name:
|
1182
|
+
indexes_by_name[idx_name] = []
|
1183
|
+
indexes_by_name[idx_name].append(idx)
|
1184
|
+
|
1185
|
+
# Display each index with its physical tables
|
1186
|
+
for idx_name, idx_tables in indexes_by_name.items():
|
1187
|
+
print(f"\n{bold('Index:')} {Colors.CYAN}{idx_name}{Colors.RESET}")
|
1188
|
+
|
1189
|
+
# Show each physical table for this index
|
1190
|
+
for idx_table in idx_tables:
|
1191
|
+
physical_table = idx_table['physical_table_name']
|
1192
|
+
table_type = idx_table['algo_table_type'] if idx_table['algo_table_type'] else 'index'
|
1193
|
+
|
1194
|
+
# Get objects for this physical table
|
1195
|
+
try:
|
1196
|
+
phys_objects = self.client.metadata.scan(database, physical_table, columns="*")
|
1197
|
+
if phys_objects:
|
1198
|
+
unique_objs = deduplicate_objects(list(phys_objects))
|
1199
|
+
total_rows = sum(getattr(obj, 'row_cnt', 0) or 0 for obj in unique_objs)
|
1200
|
+
total_null = sum(getattr(obj, 'null_cnt', 0) or 0 for obj in unique_objs)
|
1201
|
+
total_origin = sum(getattr(obj, 'origin_size', 0) or 0 for obj in unique_objs)
|
1202
|
+
total_compress = sum(
|
1203
|
+
getattr(obj, 'compress_size', 0) or 0 for obj in unique_objs
|
1204
|
+
)
|
1205
|
+
|
1206
|
+
# Color code by table type
|
1207
|
+
if table_type == 'metadata':
|
1208
|
+
type_display = f"{Colors.YELLOW}{table_type}{Colors.RESET}"
|
1209
|
+
elif table_type == 'centroids':
|
1210
|
+
type_display = f"{Colors.GREEN}{table_type}{Colors.RESET}"
|
1211
|
+
elif table_type == 'entries':
|
1212
|
+
type_display = f"{Colors.CYAN}{table_type}{Colors.RESET}"
|
1213
|
+
else:
|
1214
|
+
type_display = table_type
|
1215
|
+
|
1216
|
+
print(f" └─ Physical Table ({type_display}): {physical_table}")
|
1217
|
+
print(
|
1218
|
+
f" Objects: {len(unique_objs)} | Rows: {total_rows:,} | Null: {total_null:,} | Original: {format_size(total_origin)} | Compressed: {format_size(total_compress)}"
|
1219
|
+
)
|
1220
|
+
|
1221
|
+
# Show object details
|
1222
|
+
if unique_objs:
|
1223
|
+
print(f"\n {bold('Objects:')}")
|
1224
|
+
print(
|
1225
|
+
f" {'Object Name':<50} | {'Rows':<12} | {'Null Cnt':<10} | {'Original Size':<15} | {'Compressed Size':<15}"
|
1226
|
+
)
|
1227
|
+
print(" " + "-" * 148)
|
1228
|
+
for obj in unique_objs:
|
1229
|
+
obj_name = getattr(obj, 'object_name', 'N/A')
|
1230
|
+
rows = getattr(obj, 'row_cnt', 0) or 0
|
1231
|
+
nulls = getattr(obj, 'null_cnt', 0) or 0
|
1232
|
+
orig_size = getattr(obj, 'origin_size', 0) or 0
|
1233
|
+
comp_size = getattr(obj, 'compress_size', 0) or 0
|
1234
|
+
print(
|
1235
|
+
f" {obj_name:<50} | {rows:<12,} | {nulls:<10,} | {format_size(orig_size):<15} | {format_size(comp_size):<15}"
|
1236
|
+
)
|
1237
|
+
except Exception:
|
1238
|
+
# If can't get stats for this physical table, skip
|
1239
|
+
pass
|
1240
|
+
except Exception:
|
1241
|
+
pass
|
1242
|
+
|
1243
|
+
print("\n" + "=" * 150)
|
1244
|
+
print()
|
1245
|
+
return
|
1246
|
+
|
1247
|
+
# Regular detailed view (not hierarchical)
|
1248
|
+
# Get detailed statistics with object list
|
1249
|
+
stats = self.client.metadata.get_table_detail_stats(
|
1250
|
+
dbname=database,
|
1251
|
+
tablename=table_name,
|
1252
|
+
include_tombstone=include_tombstone,
|
1253
|
+
include_indexes=include_indexes,
|
1254
|
+
)
|
1255
|
+
|
1256
|
+
if not stats:
|
1257
|
+
print(f"⚠️ No statistics available for table '{database}.{table_name}'")
|
1258
|
+
return
|
1259
|
+
|
1260
|
+
print(f"\n📊 Detailed Table Statistics for '{database}.{table_name}':")
|
1261
|
+
print("=" * 150)
|
1262
|
+
|
1263
|
+
# Helper function to format object row
|
1264
|
+
def format_obj_row(obj):
|
1265
|
+
# Try different possible key names
|
1266
|
+
object_name = obj.get('object_name', 'N/A')
|
1267
|
+
create_ts = obj.get('create_ts', 'N/A')
|
1268
|
+
|
1269
|
+
# Safely convert to int or string for formatting
|
1270
|
+
def safe_value(value, default=0):
|
1271
|
+
# If value is already a string (formatted), return as is
|
1272
|
+
if isinstance(value, str):
|
1273
|
+
return value
|
1274
|
+
try:
|
1275
|
+
return int(value) if value is not None else default
|
1276
|
+
except (ValueError, TypeError):
|
1277
|
+
return default
|
1278
|
+
|
1279
|
+
rows_cnt = safe_value(obj.get('rows_cnt', obj.get('row_cnt', 0)))
|
1280
|
+
null_cnt = safe_value(obj.get('null_cnt', 0))
|
1281
|
+
|
1282
|
+
# Size fields might already be formatted strings (e.g., "1.5 MB")
|
1283
|
+
origin_size = obj.get('origin_size', obj.get('original_size', 0))
|
1284
|
+
compress_size = obj.get('compress_size', obj.get('compressed_size', 0))
|
1285
|
+
|
1286
|
+
# Format row based on whether sizes are numbers or strings
|
1287
|
+
if isinstance(rows_cnt, str):
|
1288
|
+
rows_str = f"{rows_cnt:<12}"
|
1289
|
+
else:
|
1290
|
+
rows_str = f"{rows_cnt:<12,}"
|
1291
|
+
|
1292
|
+
if isinstance(null_cnt, str):
|
1293
|
+
null_str = f"{null_cnt:<10}"
|
1294
|
+
else:
|
1295
|
+
null_str = f"{null_cnt:<10,}"
|
1296
|
+
|
1297
|
+
if isinstance(origin_size, str):
|
1298
|
+
orig_str = f"{origin_size:<15}"
|
1299
|
+
else:
|
1300
|
+
orig_str = f"{origin_size:<15,}"
|
1301
|
+
|
1302
|
+
if isinstance(compress_size, str):
|
1303
|
+
comp_str = f"{compress_size:<15}"
|
1304
|
+
else:
|
1305
|
+
comp_str = f"{compress_size:<15,}"
|
1306
|
+
|
1307
|
+
return f"{object_name:<50} | {create_ts:<20} | {rows_str} | {null_str} | {orig_str} | {comp_str}"
|
1308
|
+
|
1309
|
+
# Helper function to deduplicate objects by object_name
|
1310
|
+
def deduplicate_objects(objects):
|
1311
|
+
"""Deduplicate objects by object_name, keeping first occurrence"""
|
1312
|
+
seen = set()
|
1313
|
+
unique_objects = []
|
1314
|
+
for obj in objects:
|
1315
|
+
obj_name = obj.get('object_name', 'N/A')
|
1316
|
+
if obj_name not in seen:
|
1317
|
+
seen.add(obj_name)
|
1318
|
+
unique_objects.append(obj)
|
1319
|
+
return unique_objects
|
1320
|
+
|
1321
|
+
# Show main table details
|
1322
|
+
if table_name in stats:
|
1323
|
+
unique_objs = deduplicate_objects(stats[table_name])
|
1324
|
+
print(f"\n{Colors.BOLD}Table: {table_name}{Colors.RESET} ({len(unique_objs)} objects)")
|
1325
|
+
print("-" * 150)
|
1326
|
+
print(
|
1327
|
+
f"{'Object Name':<50} | {'Create Time':<20} | {'Rows':<12} | {'Null Cnt':<10} | {'Original Size':<15} | {'Compressed Size':<15}"
|
1328
|
+
)
|
1329
|
+
print("-" * 150)
|
1330
|
+
for obj in unique_objs:
|
1331
|
+
print(format_obj_row(obj))
|
1332
|
+
|
1333
|
+
# Show tombstone details
|
1334
|
+
if 'tombstone' in stats:
|
1335
|
+
unique_objs = deduplicate_objects(stats['tombstone'])
|
1336
|
+
print(f"\n{Colors.BOLD}Tombstone{Colors.RESET} ({len(unique_objs)} objects)")
|
1337
|
+
print("-" * 150)
|
1338
|
+
print(
|
1339
|
+
f"{'Object Name':<50} | {'Create Time':<20} | {'Rows':<12} | {'Null Cnt':<10} | {'Original Size':<15} | {'Compressed Size':<15}"
|
1340
|
+
)
|
1341
|
+
print("-" * 150)
|
1342
|
+
for obj in unique_objs:
|
1343
|
+
print(format_obj_row(obj))
|
1344
|
+
|
1345
|
+
# Show index details
|
1346
|
+
for key, objects in stats.items():
|
1347
|
+
if key not in [table_name, 'tombstone']:
|
1348
|
+
unique_objs = deduplicate_objects(objects)
|
1349
|
+
index_name = key.replace(f'{table_name}_', '')
|
1350
|
+
print(f"\n{Colors.BOLD}Index: {index_name}{Colors.RESET} ({len(unique_objs)} objects)")
|
1351
|
+
print("-" * 150)
|
1352
|
+
print(
|
1353
|
+
f"{'Object Name':<50} | {'Create Time':<20} | {'Rows':<12} | {'Null Cnt':<10} | {'Original Size':<15} | {'Compressed Size':<15}"
|
1354
|
+
)
|
1355
|
+
print("-" * 150)
|
1356
|
+
for obj in unique_objs:
|
1357
|
+
print(format_obj_row(obj))
|
1358
|
+
|
1359
|
+
print("=" * 150)
|
1360
|
+
else:
|
1361
|
+
# Get brief statistics (summary)
|
1362
|
+
stats = self.client.metadata.get_table_brief_stats(
|
1363
|
+
dbname=database,
|
1364
|
+
tablename=table_name,
|
1365
|
+
include_tombstone=include_tombstone,
|
1366
|
+
include_indexes=include_indexes,
|
1367
|
+
)
|
1368
|
+
|
1369
|
+
if not stats:
|
1370
|
+
print(f"⚠️ No statistics available for table '{database}.{table_name}'")
|
1371
|
+
return
|
1372
|
+
|
1373
|
+
print(f"\n📊 Table Statistics for '{database}.{table_name}':")
|
1374
|
+
print("=" * 120)
|
1375
|
+
print(
|
1376
|
+
f"{'Component':<30} | {'Objects':<10} | {'Rows':<15} | {'Null Count':<12} | {'Original Size':<15} | {'Compressed Size':<15}"
|
1377
|
+
)
|
1378
|
+
print("-" * 120)
|
1379
|
+
|
1380
|
+
# Show main table stats
|
1381
|
+
if table_name in stats:
|
1382
|
+
table_stats = stats[table_name]
|
1383
|
+
print(
|
1384
|
+
f"{table_name:<30} | {table_stats['total_objects']:<10} | {table_stats['row_cnt']:<15,} | {table_stats['null_cnt']:<12,} | {table_stats['original_size']:<15} | {table_stats['compress_size']:<15}" # type: ignore[call-overload]
|
1385
|
+
)
|
1386
|
+
|
1387
|
+
# Show tombstone stats
|
1388
|
+
if 'tombstone' in stats:
|
1389
|
+
tomb_stats = stats['tombstone']
|
1390
|
+
print(
|
1391
|
+
f"{' └─ tombstone':<30} | {tomb_stats['total_objects']:<10} | {tomb_stats['row_cnt']:<15,} | {tomb_stats['null_cnt']:<12,} | {tomb_stats['original_size']:<15} | {tomb_stats['compress_size']:<15}" # type: ignore[call-overload]
|
1392
|
+
)
|
1393
|
+
|
1394
|
+
# Show index stats
|
1395
|
+
for key, value in stats.items():
|
1396
|
+
if key not in [table_name, 'tombstone']:
|
1397
|
+
index_name = key.replace(f'{table_name}_', '') # Clean up index name if prefixed
|
1398
|
+
print(
|
1399
|
+
f"{' └─ index: ' + index_name:<30} | {value['total_objects']:<10} | {value['row_cnt']:<15,} | {value['null_cnt']:<12,} | {value['original_size']:<15} | {value['compress_size']:<15}" # type: ignore[call-overload]
|
1400
|
+
)
|
1401
|
+
|
1402
|
+
print("=" * 120)
|
1403
|
+
print(
|
1404
|
+
"\nTip: Use '-t' for tombstone, '-i idx1,idx2' for indexes, '-a' for all, '-d' for detailed object list"
|
1405
|
+
)
|
1406
|
+
print()
|
1407
|
+
|
1408
|
+
except Exception as e:
|
1409
|
+
print(f"❌ Error: {e}")
|
1410
|
+
|
1411
|
+
def do_flush_table(self, arg):
|
1412
|
+
"""Flush table and all its secondary index tables
|
1413
|
+
|
1414
|
+
Usage:
|
1415
|
+
flush_table <table> [database] - Flush main table and all its index tables
|
1416
|
+
|
1417
|
+
Example:
|
1418
|
+
flush_table my_table
|
1419
|
+
flush_table my_table test
|
1420
|
+
|
1421
|
+
Note: Requires sys user privileges
|
1422
|
+
"""
|
1423
|
+
if not arg.strip():
|
1424
|
+
print(f"{error('Error:')} Table name is required")
|
1425
|
+
print(f"{info('Usage:')} flush_table <table> [database]")
|
1426
|
+
return
|
1427
|
+
|
1428
|
+
# Parse arguments
|
1429
|
+
parts = arg.strip().split()
|
1430
|
+
if len(parts) < 1:
|
1431
|
+
print(f"{error('Error:')} Table name is required")
|
1432
|
+
return
|
1433
|
+
|
1434
|
+
table_name = parts[0].strip().rstrip(';')
|
1435
|
+
database_name = self.current_database
|
1436
|
+
|
1437
|
+
# Check for database parameter
|
1438
|
+
if len(parts) > 1:
|
1439
|
+
database_name = parts[1].strip().rstrip(';')
|
1440
|
+
|
1441
|
+
try:
|
1442
|
+
print(f"{info('🔄 Flushing table:')} {database_name}.{table_name}")
|
1443
|
+
|
1444
|
+
# Flush main table
|
1445
|
+
try:
|
1446
|
+
self.client.moctl.flush_table(database_name, table_name)
|
1447
|
+
print(f"{success('✓')} Main table flushed successfully")
|
1448
|
+
except Exception as e:
|
1449
|
+
print(f"{error('❌')} Failed to flush main table: {e}")
|
1450
|
+
return
|
1451
|
+
|
1452
|
+
# Get all index tables (including IVF/HNSW/Fulltext physical tables)
|
1453
|
+
try:
|
1454
|
+
indexes = self.client.get_table_indexes_detail(table_name, database_name)
|
1455
|
+
if indexes:
|
1456
|
+
# Extract all unique physical table names
|
1457
|
+
physical_tables = list(set(idx['physical_table_name'] for idx in indexes if idx['physical_table_name']))
|
1458
|
+
|
1459
|
+
if physical_tables:
|
1460
|
+
print(f"{info('📋 Found')} {len(physical_tables)} index physical tables")
|
1461
|
+
|
1462
|
+
# Group by index name for better display
|
1463
|
+
indexes_by_name = {}
|
1464
|
+
for idx in indexes:
|
1465
|
+
idx_name = idx['index_name']
|
1466
|
+
if idx_name not in indexes_by_name:
|
1467
|
+
indexes_by_name[idx_name] = []
|
1468
|
+
indexes_by_name[idx_name].append(idx)
|
1469
|
+
|
1470
|
+
# Flush each physical table
|
1471
|
+
success_count = 0
|
1472
|
+
for idx_name, idx_tables in indexes_by_name.items():
|
1473
|
+
print(f"\n{info('📑 Index:')} {idx_name}")
|
1474
|
+
for idx_table in idx_tables:
|
1475
|
+
physical_table = idx_table['physical_table_name']
|
1476
|
+
table_type = idx_table['algo_table_type'] if idx_table['algo_table_type'] else 'index'
|
1477
|
+
try:
|
1478
|
+
self.client.moctl.flush_table(database_name, physical_table)
|
1479
|
+
print(f" {success('✓')} {table_type}: {physical_table}")
|
1480
|
+
success_count += 1
|
1481
|
+
except Exception as e:
|
1482
|
+
print(f" {error('❌')} {table_type}: {physical_table} - {e}")
|
1483
|
+
|
1484
|
+
print(f"\n{info('📊 Summary:')}")
|
1485
|
+
print(f" Main table: {success('✓')} flushed")
|
1486
|
+
print(f" Index physical tables: {success_count}/{len(physical_tables)} flushed successfully")
|
1487
|
+
else:
|
1488
|
+
print(f"{info('ℹ️')} No index physical tables found")
|
1489
|
+
print(f"{info('📊 Summary:')} Main table: {success('✓')} flushed")
|
1490
|
+
else:
|
1491
|
+
print(f"{info('ℹ️')} No indexes found")
|
1492
|
+
print(f"{info('📊 Summary:')} Main table: {success('✓')} flushed")
|
1493
|
+
|
1494
|
+
except Exception as e:
|
1495
|
+
print(f"{warning('⚠️')} Failed to get index tables: {e}")
|
1496
|
+
print(f"{info('📊 Summary:')} Main table: {success('✓')} flushed (index tables not processed)")
|
1497
|
+
|
1498
|
+
except Exception as e:
|
1499
|
+
print(f"{error('❌ Error:')} {e}")
|
1500
|
+
|
1501
|
+
def do_tables(self, arg):
|
1502
|
+
"""Show all tables in current database or specified database
|
1503
|
+
|
1504
|
+
Usage:
|
1505
|
+
tables - Show all tables in current database
|
1506
|
+
tables <database> - Show all tables in specified database
|
1507
|
+
|
1508
|
+
Example:
|
1509
|
+
tables
|
1510
|
+
tables test
|
1511
|
+
"""
|
1512
|
+
if not self.client:
|
1513
|
+
print(f"{error('❌ Error:')} Not connected. Use 'connect' first.")
|
1514
|
+
return
|
1515
|
+
|
1516
|
+
# Parse arguments
|
1517
|
+
database_name = self.current_database
|
1518
|
+
if arg.strip():
|
1519
|
+
database_name = arg.strip().rstrip(';')
|
1520
|
+
|
1521
|
+
if not database_name:
|
1522
|
+
print(f"{error('❌ Error:')} No database specified and no current database set.")
|
1523
|
+
print(f"{info('Usage:')} tables [database]")
|
1524
|
+
return
|
1525
|
+
|
1526
|
+
try:
|
1527
|
+
# Execute SHOW TABLES
|
1528
|
+
sql = f"SHOW TABLES FROM `{database_name}`"
|
1529
|
+
result = self.client.execute(sql)
|
1530
|
+
rows = result.fetchall()
|
1531
|
+
|
1532
|
+
if rows:
|
1533
|
+
print(f"\n{header('📋 Tables in database')} '{database_name}':")
|
1534
|
+
print("=" * 80)
|
1535
|
+
|
1536
|
+
# Print table names
|
1537
|
+
for i, row in enumerate(rows, 1):
|
1538
|
+
table_name = row[0]
|
1539
|
+
print(f"{i:4}. {table_name}")
|
1540
|
+
|
1541
|
+
print("=" * 80)
|
1542
|
+
print(f"{info('Total:')} {len(rows)} tables")
|
1543
|
+
else:
|
1544
|
+
print(f"{warning('⚠️')} No tables found in database '{database_name}'")
|
1545
|
+
print()
|
1546
|
+
|
1547
|
+
except Exception as e:
|
1548
|
+
print(f"{error('❌ Error:')} {e}")
|
1549
|
+
|
1550
|
+
def do_databases(self, arg):
|
1551
|
+
"""Show all databases
|
1552
|
+
|
1553
|
+
Usage:
|
1554
|
+
databases - Show all databases
|
1555
|
+
|
1556
|
+
Example:
|
1557
|
+
databases
|
1558
|
+
"""
|
1559
|
+
if not self.client:
|
1560
|
+
print(f"{error('❌ Error:')} Not connected. Use 'connect' first.")
|
1561
|
+
return
|
1562
|
+
|
1563
|
+
try:
|
1564
|
+
# Execute SHOW DATABASES
|
1565
|
+
result = self.client.execute("SHOW DATABASES")
|
1566
|
+
rows = result.fetchall()
|
1567
|
+
|
1568
|
+
if rows:
|
1569
|
+
print(f"\n{header('🗄️ Databases:')}")
|
1570
|
+
print("=" * 80)
|
1571
|
+
|
1572
|
+
# Print database names with current database highlighted
|
1573
|
+
for i, row in enumerate(rows, 1):
|
1574
|
+
db_name = row[0]
|
1575
|
+
if db_name == self.current_database:
|
1576
|
+
print(f"{i:4}. {success(db_name)} {info('← current')}")
|
1577
|
+
else:
|
1578
|
+
print(f"{i:4}. {db_name}")
|
1579
|
+
|
1580
|
+
print("=" * 80)
|
1581
|
+
print(f"{info('Total:')} {len(rows)} databases")
|
1582
|
+
else:
|
1583
|
+
print(f"{warning('⚠️')} No databases found")
|
1584
|
+
print()
|
1585
|
+
|
1586
|
+
except Exception as e:
|
1587
|
+
print(f"{error('❌ Error:')} {e}")
|
1588
|
+
|
1589
|
+
def do_history(self, arg):
|
1590
|
+
"""Show command history
|
1591
|
+
|
1592
|
+
Usage:
|
1593
|
+
history - Show last 20 commands
|
1594
|
+
history <n> - Show last n commands
|
1595
|
+
history -c - Clear history
|
1596
|
+
|
1597
|
+
Example:
|
1598
|
+
history
|
1599
|
+
history 50
|
1600
|
+
history -c
|
1601
|
+
"""
|
1602
|
+
if not PROMPT_TOOLKIT_AVAILABLE or not self.session:
|
1603
|
+
print(f"{warning('⚠️')} History is only available when prompt_toolkit is installed.")
|
1604
|
+
print(f"{info('Tip:')} Install with: pip install prompt_toolkit")
|
1605
|
+
return
|
1606
|
+
|
1607
|
+
# Parse arguments
|
1608
|
+
arg = arg.strip()
|
1609
|
+
|
1610
|
+
# Clear history
|
1611
|
+
if arg == '-c':
|
1612
|
+
try:
|
1613
|
+
history_file = Path.home() / '.mo_diag_history'
|
1614
|
+
if history_file.exists():
|
1615
|
+
history_file.unlink()
|
1616
|
+
print(f"{success('✓')} History cleared")
|
1617
|
+
else:
|
1618
|
+
print(f"{info('ℹ️')} No history file found")
|
1619
|
+
# Recreate the history
|
1620
|
+
self.session.history = FileHistory(str(history_file))
|
1621
|
+
except Exception as e:
|
1622
|
+
print(f"{error('❌ Error:')} Failed to clear history: {e}")
|
1623
|
+
return
|
1624
|
+
|
1625
|
+
# Determine how many commands to show
|
1626
|
+
try:
|
1627
|
+
count = int(arg) if arg else 20
|
1628
|
+
except ValueError:
|
1629
|
+
print(f"{error('❌ Error:')} Invalid number: {arg}")
|
1630
|
+
return
|
1631
|
+
|
1632
|
+
# Get history
|
1633
|
+
try:
|
1634
|
+
history_file = Path.home() / '.mo_diag_history'
|
1635
|
+
if not history_file.exists():
|
1636
|
+
print(f"{info('ℹ️')} No history yet")
|
1637
|
+
return
|
1638
|
+
|
1639
|
+
# Read history file
|
1640
|
+
with open(history_file, 'r', encoding='utf-8') as f:
|
1641
|
+
lines = f.readlines()
|
1642
|
+
|
1643
|
+
# Parse prompt_toolkit FileHistory format:
|
1644
|
+
# Empty line
|
1645
|
+
# # timestamp (comment)
|
1646
|
+
# +command
|
1647
|
+
# Empty line
|
1648
|
+
commands = []
|
1649
|
+
seen = set()
|
1650
|
+
for line in lines:
|
1651
|
+
line = line.strip()
|
1652
|
+
# Skip empty lines and timestamp comments
|
1653
|
+
if not line or line.startswith('#'):
|
1654
|
+
continue
|
1655
|
+
# Commands start with '+'
|
1656
|
+
if line.startswith('+'):
|
1657
|
+
cmd = line[1:] # Remove the '+' prefix
|
1658
|
+
if cmd and cmd not in seen:
|
1659
|
+
commands.append(cmd)
|
1660
|
+
seen.add(cmd)
|
1661
|
+
|
1662
|
+
# Show last N commands
|
1663
|
+
if commands:
|
1664
|
+
start_idx = max(0, len(commands) - count)
|
1665
|
+
print(f"\n{header('📜 Command History')} (last {min(count, len(commands))} commands):")
|
1666
|
+
print("=" * 80)
|
1667
|
+
|
1668
|
+
for i, cmd in enumerate(commands[start_idx:], start=start_idx + 1):
|
1669
|
+
print(f"{i:4}. {cmd}")
|
1670
|
+
|
1671
|
+
print("=" * 80)
|
1672
|
+
print(f"{info('Total:')} {len(commands)} commands in history\n")
|
1673
|
+
else:
|
1674
|
+
print(f"{info('ℹ️')} No history yet")
|
1675
|
+
|
1676
|
+
except Exception as e:
|
1677
|
+
print(f"{error('❌ Error:')} Failed to read history: {e}")
|
1678
|
+
|
1679
|
+
def do_sql(self, arg):
|
1680
|
+
"""
|
1681
|
+
Execute a SQL query directly.
|
1682
|
+
|
1683
|
+
Usage: sql <SQL statement>
|
1684
|
+
Example: sql SELECT COUNT(*) FROM cms_all_content_chunk_info
|
1685
|
+
"""
|
1686
|
+
if not arg:
|
1687
|
+
print("❌ Usage: sql <SQL statement>")
|
1688
|
+
return
|
1689
|
+
|
1690
|
+
if not self.client:
|
1691
|
+
print("❌ Not connected. Use 'connect' first.")
|
1692
|
+
return
|
1693
|
+
|
1694
|
+
try:
|
1695
|
+
result = self.client.execute(arg)
|
1696
|
+
rows = result.fetchall()
|
1697
|
+
|
1698
|
+
if rows:
|
1699
|
+
# Print column headers if available
|
1700
|
+
try:
|
1701
|
+
# Try to get column names from result
|
1702
|
+
if hasattr(result, 'keys') and callable(result.keys):
|
1703
|
+
headers = result.keys()
|
1704
|
+
elif hasattr(result, 'columns'):
|
1705
|
+
headers = result.columns
|
1706
|
+
elif hasattr(result, '_metadata') and hasattr(result._metadata, 'keys'):
|
1707
|
+
headers = result._metadata.keys
|
1708
|
+
else:
|
1709
|
+
# Fallback: use column numbers
|
1710
|
+
headers = [f"col{i}" for i in range(len(rows[0]))]
|
1711
|
+
|
1712
|
+
print("\n" + " | ".join(str(h) for h in headers))
|
1713
|
+
print("-" * (sum(len(str(h)) for h in headers) + len(headers) * 3))
|
1714
|
+
except Exception:
|
1715
|
+
# If we can't get headers, just skip them
|
1716
|
+
pass
|
1717
|
+
|
1718
|
+
# Print rows
|
1719
|
+
for row in rows:
|
1720
|
+
print(" | ".join(str(v) if v is not None else "NULL" for v in row))
|
1721
|
+
|
1722
|
+
print(f"\n{len(rows)} row(s) returned\n")
|
1723
|
+
else:
|
1724
|
+
print("✓ Query executed successfully (0 rows returned)\n")
|
1725
|
+
|
1726
|
+
except Exception as e:
|
1727
|
+
print(f"❌ Error: {e}")
|
1728
|
+
|
1729
|
+
def do_exit(self, arg):
|
1730
|
+
"""Exit the interactive tool."""
|
1731
|
+
print("\n👋 Goodbye!\n")
|
1732
|
+
if self.client:
|
1733
|
+
try:
|
1734
|
+
self.client.disconnect()
|
1735
|
+
except Exception:
|
1736
|
+
pass
|
1737
|
+
return True
|
1738
|
+
|
1739
|
+
def do_quit(self, arg):
|
1740
|
+
"""Exit the interactive tool."""
|
1741
|
+
return self.do_exit(arg)
|
1742
|
+
|
1743
|
+
def do_EOF(self, arg):
|
1744
|
+
"""Handle Ctrl+D to exit."""
|
1745
|
+
print()
|
1746
|
+
return self.do_exit(arg)
|
1747
|
+
|
1748
|
+
def emptyline(self):
|
1749
|
+
"""Do nothing on empty line."""
|
1750
|
+
pass
|
1751
|
+
|
1752
|
+
|
1753
|
+
def start_interactive_tool(host='localhost', port=6001, user='root', password='111', database=None, log_level='ERROR'):
|
1754
|
+
"""
|
1755
|
+
Start the interactive diagnostic tool.
|
1756
|
+
|
1757
|
+
Args:
|
1758
|
+
host: Database host
|
1759
|
+
port: Database port
|
1760
|
+
user: Database user
|
1761
|
+
password: Database password
|
1762
|
+
database: Database name (optional)
|
1763
|
+
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Default: ERROR
|
1764
|
+
"""
|
1765
|
+
import logging
|
1766
|
+
|
1767
|
+
# Set logging level BEFORE creating Client
|
1768
|
+
level = getattr(logging, log_level.upper(), logging.ERROR)
|
1769
|
+
|
1770
|
+
# Set for matrixone logger and all its handlers
|
1771
|
+
mo_logger = logging.getLogger('matrixone')
|
1772
|
+
mo_logger.setLevel(level)
|
1773
|
+
|
1774
|
+
# Set level for all existing handlers
|
1775
|
+
for handler in mo_logger.handlers:
|
1776
|
+
handler.setLevel(level)
|
1777
|
+
|
1778
|
+
# Create client with minimal SQL logging
|
1779
|
+
sql_log_mode = 'off' if level >= logging.ERROR else 'auto'
|
1780
|
+
client = Client(sql_log_mode=sql_log_mode)
|
1781
|
+
|
1782
|
+
# Set level again after client creation (in case client adds handlers)
|
1783
|
+
mo_logger.setLevel(level)
|
1784
|
+
for handler in mo_logger.handlers:
|
1785
|
+
handler.setLevel(level)
|
1786
|
+
|
1787
|
+
try:
|
1788
|
+
client.connect(host=host, port=port, user=user, password=password, database=database)
|
1789
|
+
print(f"✓ Connected to {host}:{port}" + (f" (database: {database})" if database else ""))
|
1790
|
+
except Exception as e:
|
1791
|
+
print(f"⚠️ Failed to connect: {e}")
|
1792
|
+
print("You can connect manually using the 'connect' command.\n")
|
1793
|
+
client = None
|
1794
|
+
|
1795
|
+
cli = MatrixOneCLI(client)
|
1796
|
+
cli.cmdloop()
|
1797
|
+
|
1798
|
+
|
1799
|
+
def main_cli():
|
1800
|
+
"""
|
1801
|
+
Main entry point for the CLI tool when installed via pip.
|
1802
|
+
This function is called when running 'mo-diag' command.
|
1803
|
+
|
1804
|
+
Supports both interactive and non-interactive modes:
|
1805
|
+
- Interactive: mo-diag (enters interactive shell)
|
1806
|
+
- Non-interactive: mo-diag -c "show_ivf_status test" (executes single command and exits)
|
1807
|
+
"""
|
1808
|
+
import argparse
|
1809
|
+
|
1810
|
+
parser = argparse.ArgumentParser(
|
1811
|
+
description='MatrixOne Interactive Diagnostic Tool',
|
1812
|
+
epilog='''
|
1813
|
+
Examples:
|
1814
|
+
# Interactive mode
|
1815
|
+
mo-diag --host localhost --port 6001 --user root --password 111
|
1816
|
+
|
1817
|
+
# Non-interactive mode - execute single command
|
1818
|
+
mo-diag -d test -c "show_ivf_status"
|
1819
|
+
mo-diag -d test -c "show_table_stats my_table -a"
|
1820
|
+
mo-diag -d test -c "sql SELECT COUNT(*) FROM my_table"
|
1821
|
+
''',
|
1822
|
+
)
|
1823
|
+
parser.add_argument('--host', default='localhost', help='Database host (default: localhost)')
|
1824
|
+
parser.add_argument('--port', type=int, default=6001, help='Database port (default: 6001)')
|
1825
|
+
parser.add_argument('--user', default='root', help='Database user (default: root)')
|
1826
|
+
parser.add_argument('--password', default='111', help='Database password (default: 111)')
|
1827
|
+
parser.add_argument('--database', '-d', help='Database name (optional)')
|
1828
|
+
parser.add_argument(
|
1829
|
+
'--log-level',
|
1830
|
+
default='ERROR',
|
1831
|
+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
1832
|
+
help='Logging level (default: ERROR)',
|
1833
|
+
)
|
1834
|
+
parser.add_argument('--command', '-c', help='Execute a single command and exit (non-interactive mode)')
|
1835
|
+
|
1836
|
+
args = parser.parse_args()
|
1837
|
+
|
1838
|
+
# Non-interactive mode: execute single command
|
1839
|
+
if args.command:
|
1840
|
+
import logging
|
1841
|
+
|
1842
|
+
# Set logging level
|
1843
|
+
level = getattr(logging, args.log_level.upper(), logging.ERROR)
|
1844
|
+
mo_logger = logging.getLogger('matrixone')
|
1845
|
+
mo_logger.setLevel(level)
|
1846
|
+
for handler in mo_logger.handlers:
|
1847
|
+
handler.setLevel(level)
|
1848
|
+
|
1849
|
+
# Create client
|
1850
|
+
sql_log_mode = 'off' if level >= logging.ERROR else 'auto'
|
1851
|
+
client = Client(sql_log_mode=sql_log_mode)
|
1852
|
+
|
1853
|
+
# Set level again after client creation
|
1854
|
+
mo_logger.setLevel(level)
|
1855
|
+
for handler in mo_logger.handlers:
|
1856
|
+
handler.setLevel(level)
|
1857
|
+
|
1858
|
+
# Connect
|
1859
|
+
try:
|
1860
|
+
client.connect(host=args.host, port=args.port, user=args.user, password=args.password, database=args.database)
|
1861
|
+
except Exception as e:
|
1862
|
+
print(f"❌ Failed to connect: {e}")
|
1863
|
+
import sys
|
1864
|
+
|
1865
|
+
sys.exit(1)
|
1866
|
+
|
1867
|
+
# Create CLI instance and execute command
|
1868
|
+
cli = MatrixOneCLI(client)
|
1869
|
+
try:
|
1870
|
+
cli.onecmd(args.command)
|
1871
|
+
except Exception as e:
|
1872
|
+
print(f"❌ Command execution failed: {e}")
|
1873
|
+
import sys
|
1874
|
+
|
1875
|
+
sys.exit(1)
|
1876
|
+
finally:
|
1877
|
+
if client:
|
1878
|
+
try:
|
1879
|
+
client.disconnect()
|
1880
|
+
except Exception:
|
1881
|
+
pass
|
1882
|
+
else:
|
1883
|
+
# Interactive mode
|
1884
|
+
start_interactive_tool(
|
1885
|
+
host=args.host,
|
1886
|
+
port=args.port,
|
1887
|
+
user=args.user,
|
1888
|
+
password=args.password,
|
1889
|
+
database=args.database,
|
1890
|
+
log_level=args.log_level,
|
1891
|
+
)
|
1892
|
+
|
1893
|
+
|
1894
|
+
if __name__ == '__main__':
|
1895
|
+
main_cli()
|