using MarketAlly.AIPlugin;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace MarketAlly.AIPlugin.Analysis.Plugins
{
///
/// Plugin that reads and analyzes SQLite database schemas
///
[AIPlugin("SQLiteSchemaReader", "Reads and analyzes SQLite database schemas with detailed table, index, and relationship information")]
public class SQLiteSchemaReaderPlugin : IAIPlugin
{
private readonly ILogger? _logger;
///
/// Creates a new instance of SQLiteSchemaReaderPlugin
///
/// Optional logger for recording operations
public SQLiteSchemaReaderPlugin(ILogger? logger = null)
{
_logger = logger;
}
[AIParameter("Full path to the SQLite database file", required: true)]
public string DatabasePath { get; set; } = string.Empty;
[AIParameter("Include table row counts in analysis", required: false)]
public bool IncludeRowCounts { get; set; } = true;
[AIParameter("Include detailed index information", required: false)]
public bool IncludeIndexes { get; set; } = true;
[AIParameter("Include foreign key relationships", required: false)]
public bool IncludeForeignKeys { get; set; } = true;
[AIParameter("Include database metadata and statistics", required: false)]
public bool IncludeMetadata { get; set; } = true;
[AIParameter("Output format: structured, readable, json", required: false)]
public string OutputFormat { get; set; } = "structured";
[AIParameter("Maximum number of sample rows to include per table", required: false)]
public int MaxSampleRows { get; set; } = 0;
public IReadOnlyDictionary SupportedParameters => new Dictionary
{
["databasePath"] = typeof(string),
["includeRowCounts"] = typeof(bool),
["includeIndexes"] = typeof(bool),
["includeForeignKeys"] = typeof(bool),
["includeMetadata"] = typeof(bool),
["outputFormat"] = typeof(string),
["maxSampleRows"] = typeof(int)
};
public async Task ExecuteAsync(IReadOnlyDictionary parameters)
{
try
{
_logger?.LogInformation("SQLiteSchemaReader plugin executing for database {DatabasePath}", parameters["databasePath"]);
// Extract parameters
string databasePath = parameters["databasePath"].ToString() ?? string.Empty;
bool includeRowCounts = parameters.TryGetValue("includeRowCounts", out var rowCountsValue)
? Convert.ToBoolean(rowCountsValue)
: true;
bool includeIndexes = parameters.TryGetValue("includeIndexes", out var indexesValue)
? Convert.ToBoolean(indexesValue)
: true;
bool includeForeignKeys = parameters.TryGetValue("includeForeignKeys", out var fkValue)
? Convert.ToBoolean(fkValue)
: true;
bool includeMetadata = parameters.TryGetValue("includeMetadata", out var metadataValue)
? Convert.ToBoolean(metadataValue)
: true;
string outputFormat = parameters.TryGetValue("outputFormat", out var formatValue)
? formatValue?.ToString()?.ToLower() ?? "structured"
: "structured";
int maxSampleRows = parameters.TryGetValue("maxSampleRows", out var sampleValue)
? Convert.ToInt32(sampleValue)
: 0;
// Validate database file exists
if (!File.Exists(databasePath))
{
return new AIPluginResult(
new FileNotFoundException($"Database file not found: {databasePath}"),
"Database file not found"
);
}
// Read the schema
var schemaData = await ReadSchemaAsync(databasePath, includeRowCounts, includeIndexes,
includeForeignKeys, includeMetadata, maxSampleRows);
// Format output based on requested format
object result = outputFormat switch
{
"json" => schemaData,
"readable" => await GenerateReadableSchemaAsync(schemaData),
_ => schemaData // structured (default)
};
_logger?.LogInformation("Successfully analyzed SQLite database schema for {DatabasePath}, found {TableCount} tables",
databasePath, schemaData.Tables?.Count ?? 0);
return new AIPluginResult(
result,
$"Successfully analyzed SQLite database schema: {Path.GetFileName(databasePath)}"
);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to read SQLite schema from {DatabasePath}", parameters["databasePath"]);
return new AIPluginResult(ex, "Failed to read SQLite database schema");
}
}
private async Task ReadSchemaAsync(string databasePath, bool includeRowCounts,
bool includeIndexes, bool includeForeignKeys, bool includeMetadata, int maxSampleRows)
{
var connectionString = $"Data Source={databasePath}";
var schema = new DatabaseSchema
{
DatabasePath = databasePath,
DatabaseName = Path.GetFileNameWithoutExtension(databasePath),
Tables = new List()
};
using var connection = new SqliteConnection(connectionString);
await connection.OpenAsync();
// Get all tables
var tableNames = await GetTablesAsync(connection);
schema.TableCount = tableNames.Count;
foreach (var tableName in tableNames)
{
var table = new TableSchema
{
Name = tableName,
Columns = await GetTableSchemaAsync(connection, tableName)
};
if (includeIndexes)
{
table.Indexes = await GetTableIndexesAsync(connection, tableName);
}
if (includeForeignKeys)
{
table.ForeignKeys = await GetTableForeignKeysAsync(connection, tableName);
}
if (includeRowCounts)
{
table.RowCount = await GetTableRowCountAsync(connection, tableName);
}
if (maxSampleRows > 0)
{
table.SampleData = await GetSampleDataAsync(connection, tableName, maxSampleRows);
}
schema.Tables.Add(table);
}
if (includeMetadata)
{
schema.Metadata = await GetDatabaseMetadataAsync(connection, databasePath);
}
return schema;
}
private async Task> GetTablesAsync(SqliteConnection connection)
{
var tables = new List();
using var command = connection.CreateCommand();
command.CommandText = @"
SELECT name
FROM sqlite_master
WHERE type='table'
AND name NOT LIKE 'sqlite_%'
ORDER BY name";
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
tables.Add(reader.GetString(0));
}
return tables;
}
private async Task> GetTableSchemaAsync(SqliteConnection connection, string tableName)
{
var columns = new List();
using var command = connection.CreateCommand();
command.CommandText = $"PRAGMA table_info([{tableName}])";
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var column = new ColumnSchema
{
Position = reader.GetInt32(0),
Name = reader.GetString(1),
DataType = reader.GetString(2),
NotNull = reader.GetBoolean(3),
DefaultValue = reader.IsDBNull(4) ? null : reader.GetValue(4)?.ToString(),
IsPrimaryKey = reader.GetBoolean(5)
};
columns.Add(column);
}
return columns;
}
private async Task> GetTableIndexesAsync(SqliteConnection connection, string tableName)
{
var indexes = new List();
using var command = connection.CreateCommand();
command.CommandText = $"PRAGMA index_list([{tableName}])";
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var index = new IndexSchema
{
Sequence = reader.GetInt32(0),
Name = reader.GetString(1),
IsUnique = reader.GetBoolean(2),
Origin = reader.GetString(3),
IsPartial = reader.GetBoolean(4),
Columns = await GetIndexColumnsAsync(connection, reader.GetString(1))
};
indexes.Add(index);
}
return indexes;
}
private async Task> GetIndexColumnsAsync(SqliteConnection connection, string indexName)
{
var columns = new List();
using var command = connection.CreateCommand();
command.CommandText = $"PRAGMA index_info([{indexName}])";
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
columns.Add(reader.GetString(2));
}
return columns;
}
private async Task> GetTableForeignKeysAsync(SqliteConnection connection, string tableName)
{
var foreignKeys = new List();
using var command = connection.CreateCommand();
command.CommandText = $"PRAGMA foreign_key_list([{tableName}])";
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var fk = new ForeignKeySchema
{
Id = reader.GetInt32(0),
Sequence = reader.GetInt32(1),
ReferencedTable = reader.GetString(2),
FromColumn = reader.GetString(3),
ToColumn = reader.GetString(4),
OnUpdate = reader.GetString(5),
OnDelete = reader.GetString(6),
Match = reader.GetString(7)
};
foreignKeys.Add(fk);
}
return foreignKeys;
}
private async Task GetTableRowCountAsync(SqliteConnection connection, string tableName)
{
using var command = connection.CreateCommand();
command.CommandText = $"SELECT COUNT(*) FROM [{tableName}]";
var result = await command.ExecuteScalarAsync();
return Convert.ToInt64(result);
}
private async Task>> GetSampleDataAsync(SqliteConnection connection, string tableName, int maxRows)
{
var sampleData = new List>();
using var command = connection.CreateCommand();
command.CommandText = $"SELECT * FROM [{tableName}] LIMIT {maxRows}";
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var row = new Dictionary();
for (int i = 0; i < reader.FieldCount; i++)
{
row[reader.GetName(i)] = reader.IsDBNull(i) ? DBNull.Value : reader.GetValue(i);
}
sampleData.Add(row);
}
return sampleData;
}
private async Task GetDatabaseMetadataAsync(SqliteConnection connection, string databasePath)
{
var metadata = new DatabaseMetadata();
// Get SQLite version
using var versionCommand = connection.CreateCommand();
versionCommand.CommandText = "SELECT sqlite_version()";
metadata.SqliteVersion = await versionCommand.ExecuteScalarAsync() as string ?? "Unknown";
// Get database size and page info
using var sizeCommand = connection.CreateCommand();
sizeCommand.CommandText = "PRAGMA page_count; PRAGMA page_size;";
using var reader = await sizeCommand.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
metadata.PageCount = reader.GetInt64(0);
}
if (await reader.NextResultAsync() && await reader.ReadAsync())
{
metadata.PageSize = reader.GetInt64(0);
}
metadata.DatabaseSize = metadata.PageCount * metadata.PageSize;
metadata.FormattedSize = FormatBytes(metadata.DatabaseSize);
// Get file info
var fileInfo = new FileInfo(databasePath);
metadata.FileSize = fileInfo.Length;
metadata.CreatedDate = fileInfo.CreationTime;
metadata.ModifiedDate = fileInfo.LastWriteTime;
// Get encoding
using var encodingCommand = connection.CreateCommand();
encodingCommand.CommandText = "PRAGMA encoding";
metadata.Encoding = await encodingCommand.ExecuteScalarAsync() as string ?? "Unknown";
// Get journal mode
using var journalCommand = connection.CreateCommand();
journalCommand.CommandText = "PRAGMA journal_mode";
metadata.JournalMode = await journalCommand.ExecuteScalarAsync() as string ?? "Unknown";
return metadata;
}
private Task GenerateReadableSchemaAsync(DatabaseSchema schema)
{
var output = new StringBuilder();
output.AppendLine("=== SQLite Database Schema ===");
output.AppendLine($"Database: {schema.DatabaseName}");
output.AppendLine($"Path: {schema.DatabasePath}");
output.AppendLine($"Tables: {schema.TableCount}");
output.AppendLine();
foreach (var table in schema.Tables)
{
output.AppendLine($"TABLE: {table.Name}");
output.AppendLine(new string('-', 50));
foreach (var column in table.Columns)
{
var columnInfo = new StringBuilder();
columnInfo.Append($" {column.Name,-25} {column.DataType,-15}");
if (column.IsPrimaryKey) columnInfo.Append(" PRIMARY KEY");
if (column.NotNull && !column.IsPrimaryKey) columnInfo.Append(" NOT NULL");
if (!string.IsNullOrEmpty(column.DefaultValue)) columnInfo.Append($" DEFAULT {column.DefaultValue}");
output.AppendLine(columnInfo.ToString());
}
if (table.Indexes?.Count > 0)
{
output.AppendLine();
output.AppendLine(" INDEXES:");
foreach (var index in table.Indexes)
{
var indexInfo = new StringBuilder();
indexInfo.Append($" {index.Name}");
if (index.IsUnique) indexInfo.Append(" (UNIQUE)");
indexInfo.Append($" ON ({string.Join(", ", index.Columns)})");
output.AppendLine(indexInfo.ToString());
}
}
if (table.ForeignKeys?.Count > 0)
{
output.AppendLine();
output.AppendLine(" FOREIGN KEYS:");
foreach (var fk in table.ForeignKeys)
{
var fkInfo = $" {fk.FromColumn} -> {fk.ReferencedTable}.{fk.ToColumn}";
if (fk.OnUpdate != "NO ACTION") fkInfo += $" ON UPDATE {fk.OnUpdate}";
if (fk.OnDelete != "NO ACTION") fkInfo += $" ON DELETE {fk.OnDelete}";
output.AppendLine(fkInfo);
}
}
if (table.RowCount.HasValue)
{
output.AppendLine();
output.AppendLine($" ROW COUNT: {table.RowCount.Value:N0}");
}
output.AppendLine();
}
if (schema.Metadata != null)
{
output.AppendLine("=== Database Information ===");
output.AppendLine($"SQLite Version: {schema.Metadata.SqliteVersion}");
output.AppendLine($"Database Size: {schema.Metadata.FormattedSize}");
output.AppendLine($"Encoding: {schema.Metadata.Encoding}");
output.AppendLine($"Journal Mode: {schema.Metadata.JournalMode}");
output.AppendLine($"Created: {schema.Metadata.CreatedDate}");
output.AppendLine($"Modified: {schema.Metadata.ModifiedDate}");
}
return Task.FromResult(output.ToString());
}
private static string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
// Supporting data structures
public class DatabaseSchema
{
public string DatabasePath { get; set; } = string.Empty;
public string DatabaseName { get; set; } = string.Empty;
public int TableCount { get; set; }
public List Tables { get; set; } = new();
public DatabaseMetadata Metadata { get; set; } = new();
}
public class TableSchema
{
public string Name { get; set; } = string.Empty;
public List Columns { get; set; } = new();
public List Indexes { get; set; } = new();
public List ForeignKeys { get; set; } = new();
public long? RowCount { get; set; }
public List> SampleData { get; set; } = new();
}
public class ColumnSchema
{
public int Position { get; set; }
public string Name { get; set; } = string.Empty;
public string DataType { get; set; } = string.Empty;
public bool NotNull { get; set; }
public string? DefaultValue { get; set; }
public bool IsPrimaryKey { get; set; }
}
public class IndexSchema
{
public int Sequence { get; set; }
public string Name { get; set; } = string.Empty;
public bool IsUnique { get; set; }
public string Origin { get; set; } = string.Empty;
public bool IsPartial { get; set; }
public List Columns { get; set; } = new();
}
public class ForeignKeySchema
{
public int Id { get; set; }
public int Sequence { get; set; }
public string ReferencedTable { get; set; } = string.Empty;
public string FromColumn { get; set; } = string.Empty;
public string ToColumn { get; set; } = string.Empty;
public string OnUpdate { get; set; } = string.Empty;
public string OnDelete { get; set; } = string.Empty;
public string Match { get; set; } = string.Empty;
}
public class DatabaseMetadata
{
public string SqliteVersion { get; set; } = string.Empty;
public long PageCount { get; set; }
public long PageSize { get; set; }
public long DatabaseSize { get; set; }
public string FormattedSize { get; set; } = string.Empty;
public long FileSize { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
public string Encoding { get; set; } = string.Empty;
public string JournalMode { get; set; } = string.Empty;
}
}