feat: Complete Issue #14 - Database Query CLI Interface ⭐ MAJOR MILESTONE
Implement comprehensive database query interface with multiple output formats: • Add query command for executing read-only SQL queries with security constraints • Add schema command for database structure inspection • Add metadata command for file information display • Support table, JSON, and YAML output formats across all commands • Implement SQL injection prevention and safety checks • Add tabulate dependency for enhanced table formatting • Create 35 comprehensive tests covering all functionality This delivers the core USP "Relational Document Metadata" by making the database fully queryable through CLI commands with multiple output formats. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
204
markitect/cli.py
204
markitect/cli.py
@@ -19,8 +19,10 @@ import click
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from tabulate import tabulate
|
||||
|
||||
from .database import DatabaseManager
|
||||
from .document_manager import DocumentManager
|
||||
@@ -31,6 +33,53 @@ from .serializer import ASTSerializer
|
||||
pass_config = click.make_pass_decorator(dict, ensure=True)
|
||||
|
||||
|
||||
def format_output(data, output_format):
|
||||
"""
|
||||
Format data according to specified output format.
|
||||
|
||||
Args:
|
||||
data: Data to format
|
||||
output_format: Format type ('table', 'json', 'yaml')
|
||||
|
||||
Returns:
|
||||
Formatted string output
|
||||
"""
|
||||
if output_format == 'json':
|
||||
return json.dumps(data, indent=2, default=str)
|
||||
elif output_format == 'yaml':
|
||||
return yaml.dump(data, default_flow_style=False, allow_unicode=True)
|
||||
elif output_format == 'table':
|
||||
try:
|
||||
# Check if it's a list type
|
||||
if isinstance(data, (type([]), type(()))):
|
||||
if data and isinstance(data[0], dict):
|
||||
# List of dictionaries - format as table
|
||||
headers = sorted(data[0].keys())
|
||||
rows = []
|
||||
for item in data:
|
||||
row = []
|
||||
for header in headers:
|
||||
row.append(item.get(header, ''))
|
||||
rows.append(row)
|
||||
return tabulate(rows, headers=headers, tablefmt='grid')
|
||||
else:
|
||||
# List of simple values
|
||||
return tabulate([[item] for item in data], headers=['Value'], tablefmt='grid')
|
||||
elif isinstance(data, dict):
|
||||
# Single dictionary - format as key-value table
|
||||
rows = [[key, value] for key, value in data.items()]
|
||||
return tabulate(rows, headers=['Key', 'Value'], tablefmt='grid')
|
||||
else:
|
||||
# Fallback to string representation
|
||||
return str(data)
|
||||
except Exception as e:
|
||||
# Fallback to string if table formatting fails
|
||||
return f"Table formatting error: {e}\nData: {str(data)}"
|
||||
else:
|
||||
# Default to table format
|
||||
return format_output(data, 'table')
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
|
||||
@click.option('--config', 'config_file', type=click.Path(exists=True), help='Configuration file path')
|
||||
@@ -402,6 +451,161 @@ def modify(config, file_path, add_section, section_content, section_level, updat
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('sql', type=str)
|
||||
@click.option('--format', '-f', type=click.Choice(['table', 'json', 'yaml']), default='table', help='Output format')
|
||||
@pass_config
|
||||
def query(config, sql, format):
|
||||
"""
|
||||
Execute SQL query against the database.
|
||||
|
||||
Execute read-only SQL queries to explore and analyze document metadata.
|
||||
Only SELECT and WITH statements are allowed for security.
|
||||
|
||||
SQL: SQL query to execute (SELECT statements only)
|
||||
|
||||
Examples:
|
||||
markitect query "SELECT filename, created_at FROM markdown_files"
|
||||
markitect query "SELECT COUNT(*) as total FROM markdown_files" --format json
|
||||
markitect query "SELECT * FROM markdown_files WHERE filename LIKE '%.md'" --format yaml
|
||||
"""
|
||||
try:
|
||||
if config['verbose']:
|
||||
click.echo(f"Executing query: {sql}", err=True)
|
||||
|
||||
db_manager = config['db_manager']
|
||||
|
||||
# Execute the query
|
||||
results = db_manager.execute_query(sql)
|
||||
|
||||
if not results:
|
||||
if format == 'json':
|
||||
click.echo('[]')
|
||||
elif format == 'yaml':
|
||||
click.echo('[]')
|
||||
else:
|
||||
click.echo("No results found.")
|
||||
return
|
||||
|
||||
# Format and display results
|
||||
formatted_output = format_output(results, format)
|
||||
click.echo(formatted_output)
|
||||
|
||||
if config['verbose']:
|
||||
click.echo(f"Query returned {len(results)} result(s)", err=True)
|
||||
|
||||
except ValueError as e:
|
||||
click.echo(f"Query error: {e}", err=True)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
click.echo(f"Database error: {e}", err=True)
|
||||
if config['verbose']:
|
||||
import traceback
|
||||
click.echo(traceback.format_exc(), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--format', '-f', type=click.Choice(['table', 'json', 'yaml']), default='table', help='Output format')
|
||||
@pass_config
|
||||
def schema(config, format):
|
||||
"""
|
||||
Show database schema and table structure.
|
||||
|
||||
Display the structure of all tables in the database, including
|
||||
column names, types, and constraints.
|
||||
|
||||
Examples:
|
||||
markitect schema
|
||||
markitect schema --format json
|
||||
markitect schema --format yaml
|
||||
"""
|
||||
try:
|
||||
if config['verbose']:
|
||||
click.echo("Retrieving database schema...", err=True)
|
||||
|
||||
db_manager = config['db_manager']
|
||||
|
||||
# Get schema information
|
||||
schema_info = db_manager.get_schema()
|
||||
|
||||
if not schema_info:
|
||||
click.echo("No tables found in database.")
|
||||
return
|
||||
|
||||
# Format and display schema
|
||||
formatted_output = format_output(schema_info, format)
|
||||
click.echo(formatted_output)
|
||||
|
||||
if config['verbose']:
|
||||
table_count = len(schema_info)
|
||||
click.echo(f"Schema contains {table_count} table(s)", err=True)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"Schema error: {e}", err=True)
|
||||
if config['verbose']:
|
||||
import traceback
|
||||
click.echo(traceback.format_exc(), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('file_path', type=str)
|
||||
@click.option('--format', '-f', type=click.Choice(['table', 'json', 'yaml']), default='table', help='Output format')
|
||||
@pass_config
|
||||
def metadata(config, file_path, format):
|
||||
"""
|
||||
Display file metadata and front matter.
|
||||
|
||||
Show detailed information about a specific file including its
|
||||
front matter, database metadata, and processing information.
|
||||
|
||||
FILE_PATH: Name of the file to display metadata for
|
||||
|
||||
Examples:
|
||||
markitect metadata README.md
|
||||
markitect metadata docs/guide.md --format json
|
||||
markitect metadata config.md --format yaml
|
||||
"""
|
||||
try:
|
||||
if config['verbose']:
|
||||
click.echo(f"Retrieving metadata for: {file_path}", err=True)
|
||||
|
||||
db_manager = config['db_manager']
|
||||
|
||||
# Get file information from database
|
||||
file_info = db_manager.get_markdown_file(file_path)
|
||||
|
||||
if not file_info:
|
||||
click.echo(f"File not found in database: {file_path}", err=True)
|
||||
click.echo("Use 'markitect ingest' to process the file first.", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse front matter for better display
|
||||
if file_info.get('front_matter'):
|
||||
try:
|
||||
if isinstance(file_info['front_matter'], str):
|
||||
file_info['front_matter'] = eval(file_info['front_matter'])
|
||||
except (ValueError, TypeError, SyntaxError):
|
||||
if config['verbose']:
|
||||
click.echo("Warning: Could not parse front matter", err=True)
|
||||
|
||||
# Format and display metadata
|
||||
formatted_output = format_output(file_info, format)
|
||||
click.echo(formatted_output)
|
||||
|
||||
if config['verbose']:
|
||||
content_length = len(file_info.get('content', ''))
|
||||
click.echo(f"Content length: {content_length} characters", err=True)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"Metadata error: {e}", err=True)
|
||||
if config['verbose']:
|
||||
import traceback
|
||||
click.echo(traceback.format_exc(), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@pass_config
|
||||
def list(config):
|
||||
|
||||
Reference in New Issue
Block a user