chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using Strata.SqlTools.EFCore.Models;
|
||||
|
||||
namespace Strata.SqlTools.EFCore.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for converting between QueryBreakdown and QueryBreakdownEntity for Entity Framework Core integration.
|
||||
/// </summary>
|
||||
public interface IQueryBreakdownMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a QueryBreakdown (SQL Tools) to a QueryBreakdownEntity (EF Core).
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The QueryBreakdown to convert.</param>
|
||||
/// <returns>A QueryBreakdownEntity that can be persisted to the database.</returns>
|
||||
QueryBreakdownEntity MapToEntity(QueryBreakdown queryBreakdown);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a QueryBreakdownEntity (EF Core) back to a QueryBreakdown (SQL Tools).
|
||||
/// </summary>
|
||||
/// <param name="entity">The QueryBreakdownEntity to convert.</param>
|
||||
/// <returns>A QueryBreakdown instance with all clauses and parameters restored.</returns>
|
||||
QueryBreakdown MapToDomainModel(QueryBreakdownEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a QueryBreakdown to a QueryBreakdownEntity with related entities (parameters and WITH clauses).
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The QueryBreakdown to convert.</param>
|
||||
/// <returns>A QueryBreakdownEntity with all related entities populated.</returns>
|
||||
(QueryBreakdownEntity Entity, List<QueryParameterEntity> Parameters, List<WithClauseEntity> WithClauses) MapToEntityWithRelations(QueryBreakdown queryBreakdown);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a QueryBreakdownEntity with related entities back to a QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="entity">The QueryBreakdownEntity with navigation properties loaded.</param>
|
||||
/// <returns>A fully reconstructed QueryBreakdown instance.</returns>
|
||||
QueryBreakdown MapToDomainModelWithRelations(QueryBreakdownEntity entity);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Strata.SqlTools.EFCore.Models;
|
||||
|
||||
namespace Strata.SqlTools.EFCore.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Entity Framework Core configuration for the QueryBreakdownEntity.
|
||||
/// Defines the table structure, relationships, and constraints.
|
||||
/// </summary>
|
||||
public class QueryBreakdownEntityConfiguration : IEntityTypeConfiguration<QueryBreakdownEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the QueryBreakdownEntity for Entity Framework Core.
|
||||
/// </summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<QueryBreakdownEntity> builder)
|
||||
{
|
||||
builder.ToTable("QueryBreakdowns");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||
|
||||
// Configure SELECT clause
|
||||
builder.Property(e => e.SelectClause)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.SelectClauseComment)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure FROM clause
|
||||
builder.Property(e => e.FromClause)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.FromClauseComment)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure WHERE clause
|
||||
builder.Property(e => e.WhereClause)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.WhereClauseComment)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure GROUP BY clause
|
||||
builder.Property(e => e.GroupByClause)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.GroupByClauseComment)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure HAVING clause
|
||||
builder.Property(e => e.HavingClause)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.HavingClauseComment)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure ORDER BY clause
|
||||
builder.Property(e => e.OrderByClause)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.OrderByClauseComment)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure WITH clause (CTEs)
|
||||
builder.Property(e => e.WithClause)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure raw SQL
|
||||
builder.Property(e => e.RawSql)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure JSON properties
|
||||
builder.Property(e => e.SetupClausesJson)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.FinishClausesJson)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.ParametersJson)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Configure timestamps
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("GETUTCDATE()");
|
||||
|
||||
builder.Property(e => e.UpdatedAt)
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("GETUTCDATE()");
|
||||
|
||||
// Configure relationships
|
||||
builder.HasMany<QueryParameterEntity>()
|
||||
.WithOne(p => p.QueryBreakdownEntity)
|
||||
.HasForeignKey(p => p.QueryBreakdownEntityId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany<WithClauseEntity>()
|
||||
.WithOne(w => w.QueryBreakdownEntity)
|
||||
.HasForeignKey(w => w.QueryBreakdownEntityId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Create indexes for common queries
|
||||
builder.HasIndex(e => e.CreatedAt);
|
||||
builder.HasIndex(e => e.UpdatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Strata.SqlTools.EFCore.Models;
|
||||
|
||||
namespace Strata.SqlTools.EFCore.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Entity Framework Core configuration for the QueryParameterEntity.
|
||||
/// </summary>
|
||||
public class QueryParameterEntityConfiguration : IEntityTypeConfiguration<QueryParameterEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the QueryParameterEntity for Entity Framework Core.
|
||||
/// </summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<QueryParameterEntity> builder)
|
||||
{
|
||||
builder.ToTable("QueryParameters");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||
|
||||
builder.Property(e => e.QueryBreakdownEntityId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.ParameterName)
|
||||
.HasColumnType("nvarchar(256)")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.ParameterValue)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.ParameterTypeName)
|
||||
.HasColumnType("nvarchar(256)")
|
||||
.IsRequired(false);
|
||||
|
||||
// Create index for faster lookups
|
||||
builder.HasIndex(e => new { e.QueryBreakdownEntityId, e.ParameterName })
|
||||
.IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Strata.SqlTools.EFCore.Models;
|
||||
|
||||
namespace Strata.SqlTools.EFCore.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Entity Framework Core configuration for the WithClauseEntity.
|
||||
/// </summary>
|
||||
public class WithClauseEntityConfiguration : IEntityTypeConfiguration<WithClauseEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the WithClauseEntity for Entity Framework Core.
|
||||
/// </summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<WithClauseEntity> builder)
|
||||
{
|
||||
builder.ToTable("WithClauses");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||
|
||||
builder.Property(e => e.QueryBreakdownEntityId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.CteName)
|
||||
.HasColumnType("nvarchar(256)")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.ColumnList)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(e => e.CteDefinition)
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.OrderIndex)
|
||||
.IsRequired();
|
||||
|
||||
// Create index for ordering and lookups
|
||||
builder.HasIndex(e => new { e.QueryBreakdownEntityId, e.OrderIndex });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
namespace Strata.SqlTools.EFCore.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SQL query breakdown entity for Entity Framework Core mapping.
|
||||
/// This entity encapsulates the query components (SELECT, FROM, WHERE, etc.)
|
||||
/// and is designed to be compatible with EF Core DbContext and database models.
|
||||
/// </summary>
|
||||
public class QueryBreakdownEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this query breakdown.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SELECT clause of the query.
|
||||
/// </summary>
|
||||
public string? SelectClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comment for the SELECT clause.
|
||||
/// </summary>
|
||||
public string? SelectClauseComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FROM clause of the query.
|
||||
/// </summary>
|
||||
public string? FromClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comment for the FROM clause.
|
||||
/// </summary>
|
||||
public string? FromClauseComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WHERE clause of the query.
|
||||
/// </summary>
|
||||
public string? WhereClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comment for the WHERE clause.
|
||||
/// </summary>
|
||||
public string? WhereClauseComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the GROUP BY clause of the query.
|
||||
/// </summary>
|
||||
public string? GroupByClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comment for the GROUP BY clause.
|
||||
/// </summary>
|
||||
public string? GroupByClauseComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HAVING clause of the query.
|
||||
/// </summary>
|
||||
public string? HavingClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comment for the HAVING clause.
|
||||
/// </summary>
|
||||
public string? HavingClauseComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ORDER BY clause of the query.
|
||||
/// </summary>
|
||||
public string? OrderByClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comment for the ORDER BY clause.
|
||||
/// </summary>
|
||||
public string? OrderByClauseComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WITH clause (Common Table Expressions) as a JSON string.
|
||||
/// </summary>
|
||||
public string? WithClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw/original SQL statement before parsing and breakdown.
|
||||
/// </summary>
|
||||
public string? RawSql { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the setup clauses as a JSON string.
|
||||
/// These are clauses to execute before the main statement.
|
||||
/// </summary>
|
||||
public string? SetupClausesJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the finish clauses as a JSON string.
|
||||
/// These are clauses to execute after the main statement.
|
||||
/// </summary>
|
||||
public string? FinishClausesJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameters as a JSON string.
|
||||
/// Contains parameter names and their values.
|
||||
/// </summary>
|
||||
public string? ParametersJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when this entity was created.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when this entity was last updated.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property for the related query parameters.
|
||||
/// </summary>
|
||||
public virtual ICollection<QueryParameterEntity> Parameters { get; set; } = new List<QueryParameterEntity>();
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property for the related WITH clauses (CTEs).
|
||||
/// </summary>
|
||||
public virtual ICollection<WithClauseEntity> WithClauses { get; set; } = new List<WithClauseEntity>();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Strata.SqlTools.EFCore.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a query parameter entity for use with Entity Framework Core.
|
||||
/// Stores query parameter names and their values with type information.
|
||||
/// </summary>
|
||||
public class QueryParameterEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this parameter.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the parent query breakdown entity.
|
||||
/// </summary>
|
||||
public int QueryBreakdownEntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter name (e.g., "@ParameterName" or "ParameterName").
|
||||
/// </summary>
|
||||
public string ParameterName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter value as a string representation.
|
||||
/// </summary>
|
||||
public string? ParameterValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CLR type name of the parameter value for deserialization.
|
||||
/// </summary>
|
||||
public string? ParameterTypeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property to the parent QueryBreakdownEntity.
|
||||
/// </summary>
|
||||
public virtual QueryBreakdownEntity? QueryBreakdownEntity { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Strata.SqlTools.EFCore.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a WITH clause (Common Table Expression) entity for Entity Framework Core mapping.
|
||||
/// </summary>
|
||||
public class WithClauseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this WITH clause.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the parent query breakdown entity.
|
||||
/// </summary>
|
||||
public int QueryBreakdownEntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the CTE (Common Table Expression).
|
||||
/// </summary>
|
||||
public string CteName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the column list for the CTE (optional).
|
||||
/// </summary>
|
||||
public string? ColumnList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the definition/query of the CTE.
|
||||
/// </summary>
|
||||
public string CteDefinition { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the order of this CTE in the WITH clause.
|
||||
/// </summary>
|
||||
public int OrderIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property to the parent QueryBreakdownEntity.
|
||||
/// </summary>
|
||||
public virtual QueryBreakdownEntity? QueryBreakdownEntity { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
# Strata.SqlTools.EFCore
|
||||
|
||||
> Entity Framework Core integration and support for Strata.SqlTools QueryBreakdown functionality
|
||||
|
||||
This project provides seamless integration between the Strata.SqlTools query analysis framework and Entity Framework Core, allowing you to persist, query, and manage `QueryBreakdown` objects within your existing EF Core DbContext.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **EF Core Integration**: Map QueryBreakdown objects directly to your DbContext
|
||||
- **Entity Models**: Fully normalized entity models for QueryBreakdownEntity, QueryParameterEntity, and WithClauseEntity
|
||||
- **Automatic Mapping**: IQueryBreakdownMapper for converting between SQL Tools and EF Core models
|
||||
- **Repository Pattern**: IQueryBreakdownRepository for simplified CRUD operations
|
||||
- **DbContext Extensions**: Easy-to-use extension methods for DbContext integration
|
||||
- **JSON Serialization**: Intelligent serialization of complex types (parameters, clauses) to JSON for efficient storage
|
||||
|
||||
## Installation
|
||||
|
||||
Add the NuGet package reference:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="Strata.SqlTools.EFCore" Version="1.0.0" />
|
||||
```
|
||||
|
||||
Or via the .NET CLI:
|
||||
|
||||
```bash
|
||||
dotnet add package Strata.SqlTools.EFCore
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Configure Your DbContext
|
||||
|
||||
Add the QueryBreakdown entities to your DbContext:
|
||||
|
||||
```csharp
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Strata.SqlTools.EFCore.Models;
|
||||
using Strata.SqlTools.EFCore.Configurations;
|
||||
|
||||
public class YourDbContext : DbContext
|
||||
{
|
||||
public DbSet<QueryBreakdownEntity> QueryBreakdowns { get; set; }
|
||||
public DbSet<QueryParameterEntity> QueryParameters { get; set; }
|
||||
public DbSet<WithClauseEntity> WithClauses { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Configure QueryBreakdown entities
|
||||
modelBuilder.ConfigureQueryBreakdownEntities();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register Services
|
||||
|
||||
Register the mapper and repository in your dependency injection container:
|
||||
|
||||
```csharp
|
||||
services.AddScoped<IQueryBreakdownMapper, QueryBreakdownMapper>();
|
||||
services.AddScoped<IQueryBreakdownRepository>(
|
||||
provider => new QueryBreakdownRepository(
|
||||
provider.GetRequiredService<YourDbContext>(),
|
||||
provider.GetRequiredService<IQueryBreakdownMapper>()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Use the Repository
|
||||
|
||||
Inject and use the repository in your application:
|
||||
|
||||
```csharp
|
||||
public class QueryService
|
||||
{
|
||||
private readonly IQueryBreakdownRepository _repository;
|
||||
|
||||
public QueryService(IQueryBreakdownRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task SaveQueryAsync(QueryBreakdown queryBreakdown)
|
||||
{
|
||||
int id = await _repository.AddAsync(queryBreakdown);
|
||||
Console.WriteLine($"Query saved with ID: {id}");
|
||||
}
|
||||
|
||||
public async Task<QueryBreakdown?> GetQueryAsync(int id)
|
||||
{
|
||||
return await _repository.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
public async Task<List<QueryBreakdown>> GetAllQueriesAsync()
|
||||
{
|
||||
return await _repository.GetAllAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateQueryAsync(int id, QueryBreakdown queryBreakdown)
|
||||
{
|
||||
await _repository.UpdateAsync(id, queryBreakdown);
|
||||
}
|
||||
|
||||
public async Task DeleteQueryAsync(int id)
|
||||
{
|
||||
bool deleted = await _repository.DeleteAsync(id);
|
||||
Console.WriteLine(deleted ? "Query deleted." : "Query not found.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Models
|
||||
|
||||
### QueryBreakdownEntity
|
||||
|
||||
The main entity that represents a SQL query breakdown:
|
||||
|
||||
- **Id**: Primary key
|
||||
- **SelectClause**: The SELECT clause
|
||||
- **FromClause**: The FROM clause
|
||||
- **WhereClause**: The WHERE clause
|
||||
- **GroupByClause**: The GROUP BY clause
|
||||
- **HavingClause**: The HAVING clause
|
||||
- **OrderByClause**: The ORDER BY clause
|
||||
- **WithClause**: Common Table Expressions (CTEs)
|
||||
- **RawSql**: Original SQL statement
|
||||
- **SetupClausesJson**: JSON serialized setup clauses
|
||||
- **FinishClausesJson**: JSON serialized finish clauses
|
||||
- **ParametersJson**: JSON serialized parameters
|
||||
- **CreatedAt**: Creation timestamp
|
||||
- **UpdatedAt**: Last update timestamp
|
||||
|
||||
#### Related Entities
|
||||
|
||||
- **QueryParameterEntity**: Represents parameters used in the query
|
||||
- **WithClauseEntity**: Represents individual Common Table Expressions (CTEs)
|
||||
|
||||
## Mapper Interface
|
||||
|
||||
The `IQueryBreakdownMapper` provides the following operations:
|
||||
|
||||
```csharp
|
||||
public interface IQueryBreakdownMapper
|
||||
{
|
||||
QueryBreakdownEntity MapToEntity(QueryBreakdown queryBreakdown);
|
||||
QueryBreakdown MapToDomainModel(QueryBreakdownEntity entity);
|
||||
(QueryBreakdownEntity Entity, List<QueryParameterEntity> Parameters, List<WithClauseEntity> WithClauses) MapToEntityWithRelations(QueryBreakdown queryBreakdown);
|
||||
QueryBreakdown MapToDomainModelWithRelations(QueryBreakdownEntity entity);
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Interface
|
||||
|
||||
The `IQueryBreakdownRepository` provides the following operations:
|
||||
|
||||
```csharp
|
||||
public interface IQueryBreakdownRepository
|
||||
{
|
||||
Task<int> AddAsync(QueryBreakdown queryBreakdown);
|
||||
Task<QueryBreakdown?> GetByIdAsync(int id);
|
||||
Task<QueryBreakdownEntity?> GetEntityByIdAsync(int id);
|
||||
Task<List<QueryBreakdown>> GetAllAsync();
|
||||
Task<List<QueryBreakdownEntity>> GetAllEntitiesAsync();
|
||||
Task UpdateAsync(int id, QueryBreakdown queryBreakdown);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
Task<int> GetCountAsync();
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
The project includes three main tables:
|
||||
|
||||
### QueryBreakdowns Table
|
||||
Stores the main query breakdown information
|
||||
|
||||
### QueryParameters Table
|
||||
Stores individual query parameters with foreign key to QueryBreakdowns
|
||||
|
||||
### WithClauses Table
|
||||
Stores Common Table Expressions with foreign key to QueryBreakdowns
|
||||
|
||||
## DbContext Extension Methods
|
||||
|
||||
```csharp
|
||||
// Configure query breakdown entities during model creation
|
||||
modelBuilder.ConfigureQueryBreakdownEntities();
|
||||
|
||||
// Get queryable sets from context
|
||||
var queryBreakdowns = dbContext.GetQueryBreakdowns();
|
||||
var parameters = dbContext.GetQueryParameters();
|
||||
var withClauses = dbContext.GetWithClauses();
|
||||
|
||||
// Get a query breakdown with related data
|
||||
var entity = await dbContext.GetQueryBreakdownWithRelatedDataAsync(id);
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Entity Configuration
|
||||
|
||||
If you need to customize the entity configuration, you can create your own configuration classes that implement `IEntityTypeConfiguration<T>`:
|
||||
|
||||
```csharp
|
||||
public class CustomQueryBreakdownConfiguration : IEntityTypeConfiguration<QueryBreakdownEntity>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<QueryBreakdownEntity> builder)
|
||||
{
|
||||
// Apply custom configuration
|
||||
builder.ToTable("CustomQueryBreakdowns", "dbo");
|
||||
// ... other configurations
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Working with Existing DbContext
|
||||
|
||||
If you already have an existing DbContext, simply:
|
||||
|
||||
1. Add the DbSets for QueryBreakdown entities
|
||||
2. Call `modelBuilder.ConfigureQueryBreakdownEntities()` in `OnModelCreating`
|
||||
3. Create a migration: `dotnet ef migrations add AddQueryBreakdownEntities`
|
||||
4. Update the database: `dotnet ef database update`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Microsoft.EntityFrameworkCore** (8.0.0+)
|
||||
- **Microsoft.EntityFrameworkCore.Relational** (8.0.0+)
|
||||
- **Strata.SqlTools** (1.0.0+)
|
||||
- **Strata.SqlTools.SqlServer** (1.0.0+)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Support
|
||||
|
||||
For issues, feature requests, or questions, please visit the [GitHub repository](https://github.com/stratadecision/sql-builder).
|
||||
@@ -0,0 +1,74 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Strata.SqlTools.EFCore.Models;
|
||||
|
||||
namespace Strata.SqlTools.EFCore.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for DbContext to support QueryBreakdown entities.
|
||||
/// </summary>
|
||||
public static class DbContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an entity configuration to the ModelBuilder for QueryBreakdown-related entities.
|
||||
/// Call this in your DbContext.OnModelCreating method.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The ModelBuilder instance.</param>
|
||||
/// <returns>The ModelBuilder instance for fluent chaining.</returns>
|
||||
public static ModelBuilder ConfigureQueryBreakdownEntities(this ModelBuilder modelBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modelBuilder);
|
||||
|
||||
modelBuilder.ApplyConfiguration(new Configurations.QueryBreakdownEntityConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new Configurations.QueryParameterEntityConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new Configurations.WithClauseEntityConfiguration());
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a queryable set of QueryBreakdownEntity instances from the DbContext.
|
||||
/// </summary>
|
||||
/// <param name="context">The DbContext instance.</param>
|
||||
/// <returns>An IQueryable of QueryBreakdownEntity.</returns>
|
||||
public static IQueryable<QueryBreakdownEntity> GetQueryBreakdowns(this DbContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
return context.Set<QueryBreakdownEntity>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a queryable set of QueryParameterEntity instances from the DbContext.
|
||||
/// </summary>
|
||||
/// <param name="context">The DbContext instance.</param>
|
||||
/// <returns>An IQueryable of QueryParameterEntity.</returns>
|
||||
public static IQueryable<QueryParameterEntity> GetQueryParameters(this DbContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
return context.Set<QueryParameterEntity>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a queryable set of WithClauseEntity instances from the DbContext.
|
||||
/// </summary>
|
||||
/// <param name="context">The DbContext instance.</param>
|
||||
/// <returns>An IQueryable of WithClauseEntity.</returns>
|
||||
public static IQueryable<WithClauseEntity> GetWithClauses(this DbContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
return context.Set<WithClauseEntity>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Includes query breakdown related data and returns a single QueryBreakdownEntity by ID.
|
||||
/// </summary>
|
||||
/// <param name="context">The DbContext instance.</param>
|
||||
/// <param name="id">The ID of the QueryBreakdownEntity to retrieve.</param>
|
||||
/// <returns>The QueryBreakdownEntity with related entities included, or null if not found.</returns>
|
||||
public static async Task<QueryBreakdownEntity?> GetQueryBreakdownWithRelatedDataAsync(this DbContext context, int id)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
return await context.Set<QueryBreakdownEntity>()
|
||||
.FirstOrDefaultAsync(q => q.Id == id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
using System.Collections;
|
||||
using System.Text.Json;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using Strata.SqlTools.EFCore.Abstractions;
|
||||
using Strata.SqlTools.EFCore.Models;
|
||||
|
||||
namespace Strata.SqlTools.EFCore.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of IQueryBreakdownMapper for converting between QueryBreakdown and QueryBreakdownEntity.
|
||||
/// </summary>
|
||||
public class QueryBreakdownMapper : IQueryBreakdownMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a QueryBreakdown (SQL Tools) to a QueryBreakdownEntity (EF Core).
|
||||
/// </summary>
|
||||
public QueryBreakdownEntity MapToEntity(QueryBreakdown queryBreakdown)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queryBreakdown);
|
||||
|
||||
var entity = new QueryBreakdownEntity
|
||||
{
|
||||
SelectClause = queryBreakdown.SelectClause?.Clause,
|
||||
SelectClauseComment = queryBreakdown.SelectClause?.Comment,
|
||||
FromClause = queryBreakdown.FromClause?.Clause,
|
||||
FromClauseComment = queryBreakdown.FromClause?.Comment,
|
||||
WhereClause = queryBreakdown.WhereClause?.Clause,
|
||||
WhereClauseComment = queryBreakdown.WhereClause?.Comment,
|
||||
GroupByClause = queryBreakdown.GroupByClause?.Clause,
|
||||
GroupByClauseComment = queryBreakdown.GroupByClause?.Comment,
|
||||
HavingClause = queryBreakdown.HavingClause?.Clause,
|
||||
HavingClauseComment = queryBreakdown.HavingClause?.Comment,
|
||||
OrderByClause = queryBreakdown.OrderByClause?.Clause,
|
||||
OrderByClauseComment = queryBreakdown.OrderByClause?.Comment,
|
||||
WithClause = queryBreakdown.GetWithClauseValue(),
|
||||
RawSql = queryBreakdown.RawSql,
|
||||
SetupClausesJson = SerializeList(queryBreakdown.SetupClauses),
|
||||
FinishClausesJson = SerializeArrayList(queryBreakdown.FinishClauses),
|
||||
ParametersJson = SerializeDictionary(queryBreakdown.Parameters),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a QueryBreakdownEntity (EF Core) back to a QueryBreakdown (SQL Tools).
|
||||
/// </summary>
|
||||
public QueryBreakdown MapToDomainModel(QueryBreakdownEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
var queryBreakdown = new QueryBreakdown();
|
||||
|
||||
// Set clause properties
|
||||
if (!string.IsNullOrEmpty(entity.SelectClause))
|
||||
{
|
||||
queryBreakdown.SelectClause.Clause = entity.SelectClause;
|
||||
queryBreakdown.SelectClause.Comment = entity.SelectClauseComment;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.FromClause))
|
||||
{
|
||||
queryBreakdown.FromClause.Clause = entity.FromClause;
|
||||
queryBreakdown.FromClause.Comment = entity.FromClauseComment;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.WhereClause))
|
||||
{
|
||||
queryBreakdown.WhereClause.Clause = entity.WhereClause;
|
||||
queryBreakdown.WhereClause.Comment = entity.WhereClauseComment;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.GroupByClause))
|
||||
{
|
||||
queryBreakdown.GroupByClause.Clause = entity.GroupByClause;
|
||||
queryBreakdown.GroupByClause.Comment = entity.GroupByClauseComment;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.HavingClause))
|
||||
{
|
||||
queryBreakdown.HavingClause.Clause = entity.HavingClause;
|
||||
queryBreakdown.HavingClause.Comment = entity.HavingClauseComment;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.OrderByClause))
|
||||
{
|
||||
queryBreakdown.OrderByClause.Clause = entity.OrderByClause;
|
||||
queryBreakdown.OrderByClause.Comment = entity.OrderByClauseComment;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.WithClause))
|
||||
{
|
||||
queryBreakdown.SetWithClauseValue(entity.WithClause);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.RawSql))
|
||||
{
|
||||
queryBreakdown.RawSql = entity.RawSql;
|
||||
}
|
||||
|
||||
// Restore setup clauses
|
||||
if (!string.IsNullOrEmpty(entity.SetupClausesJson))
|
||||
{
|
||||
var setupClauses = DeserializeList(entity.SetupClausesJson);
|
||||
queryBreakdown.SetupClauses.Clear();
|
||||
foreach (var clause in setupClauses)
|
||||
{
|
||||
queryBreakdown.SetupClauses.Add(clause);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore finish clauses
|
||||
if (!string.IsNullOrEmpty(entity.FinishClausesJson))
|
||||
{
|
||||
var finishClauses = DeserializeArrayList(entity.FinishClausesJson);
|
||||
queryBreakdown.FinishClauses.Clear();
|
||||
foreach (var clause in finishClauses)
|
||||
{
|
||||
queryBreakdown.FinishClauses.Add(clause);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore parameters
|
||||
if (!string.IsNullOrEmpty(entity.ParametersJson))
|
||||
{
|
||||
var parameters = DeserializeDictionary(entity.ParametersJson);
|
||||
queryBreakdown.Parameters.Clear();
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
queryBreakdown.Parameters[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return queryBreakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a QueryBreakdown to a QueryBreakdownEntity with related entities.
|
||||
/// </summary>
|
||||
public (QueryBreakdownEntity Entity, List<QueryParameterEntity> Parameters, List<WithClauseEntity> WithClauses) MapToEntityWithRelations(QueryBreakdown queryBreakdown)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queryBreakdown);
|
||||
|
||||
var entity = MapToEntity(queryBreakdown);
|
||||
|
||||
// Map parameters
|
||||
var parameterEntities = new List<QueryParameterEntity>();
|
||||
foreach (var param in queryBreakdown.ParameterList)
|
||||
{
|
||||
parameterEntities.Add(new QueryParameterEntity
|
||||
{
|
||||
ParameterName = param.Name,
|
||||
ParameterValue = param.Value?.ToString(),
|
||||
ParameterTypeName = param.Value?.GetType().FullName
|
||||
});
|
||||
}
|
||||
|
||||
// Map WITH clauses
|
||||
var withClauseEntities = new List<WithClauseEntity>();
|
||||
int orderIndex = 0;
|
||||
foreach (var withClause in queryBreakdown.WithClauses)
|
||||
{
|
||||
withClauseEntities.Add(new WithClauseEntity
|
||||
{
|
||||
CteName = withClause.TableName,
|
||||
ColumnList = withClause.Clause,
|
||||
CteDefinition = withClause.Sql?.SelectClause?.Clause ?? string.Empty,
|
||||
OrderIndex = orderIndex++
|
||||
});
|
||||
}
|
||||
|
||||
return (entity, parameterEntities, withClauseEntities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a QueryBreakdownEntity with related entities back to a QueryBreakdown.
|
||||
/// </summary>
|
||||
public QueryBreakdown MapToDomainModelWithRelations(QueryBreakdownEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
var queryBreakdown = MapToDomainModel(entity);
|
||||
|
||||
// Reconstruct Parameters dictionary from parameter entities if they are loaded
|
||||
if (entity.Parameters != null && entity.Parameters.Count > 0)
|
||||
{
|
||||
queryBreakdown.Parameters.Clear();
|
||||
foreach (var paramEntity in entity.Parameters)
|
||||
{
|
||||
// Store with @ prefix to match how AddParameter works
|
||||
var key = paramEntity.ParameterName.StartsWith('@')
|
||||
? paramEntity.ParameterName
|
||||
: $"@{paramEntity.ParameterName}";
|
||||
|
||||
// Deserialize value if type information is available
|
||||
object? value = paramEntity.ParameterValue;
|
||||
if (!string.IsNullOrEmpty(paramEntity.ParameterTypeName) && !string.IsNullOrEmpty(paramEntity.ParameterValue))
|
||||
{
|
||||
var type = Type.GetType(paramEntity.ParameterTypeName);
|
||||
if (type != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = Convert.ChangeType(paramEntity.ParameterValue, type);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If conversion fails, use string value
|
||||
value = paramEntity.ParameterValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryBreakdown.Parameters[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return queryBreakdown;
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
internal static string SerializeList(List<string> list)
|
||||
{
|
||||
return JsonSerializer.Serialize(list);
|
||||
}
|
||||
|
||||
internal static List<string> DeserializeList(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
|
||||
}
|
||||
|
||||
internal static string SerializeArrayList(ArrayList list)
|
||||
{
|
||||
var stringList = new List<string>();
|
||||
foreach (var item in list)
|
||||
{
|
||||
stringList.Add(item?.ToString() ?? string.Empty);
|
||||
}
|
||||
return JsonSerializer.Serialize(stringList);
|
||||
}
|
||||
|
||||
internal static ArrayList DeserializeArrayList(string json)
|
||||
{
|
||||
var stringList = JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
|
||||
var arrayList = new ArrayList();
|
||||
foreach (var item in stringList)
|
||||
{
|
||||
arrayList.Add(item);
|
||||
}
|
||||
return arrayList;
|
||||
}
|
||||
|
||||
internal static string SerializeDictionary(Dictionary<string, object> dict)
|
||||
{
|
||||
var stringDict = new Dictionary<string, string>();
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
stringDict[kvp.Key] = kvp.Value?.ToString() ?? string.Empty;
|
||||
}
|
||||
return JsonSerializer.Serialize(stringDict);
|
||||
}
|
||||
|
||||
internal static Dictionary<string, object> DeserializeDictionary(string json)
|
||||
{
|
||||
var stringDict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
|
||||
var result = new Dictionary<string, object>();
|
||||
foreach (var kvp in stringDict)
|
||||
{
|
||||
result[kvp.Key] = kvp.Value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using Strata.SqlTools.EFCore.Abstractions;
|
||||
using Strata.SqlTools.EFCore.Models;
|
||||
|
||||
namespace Strata.SqlTools.EFCore.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a generic repository pattern for QueryBreakdown entities.
|
||||
/// Provides a simplified API for common database operations.
|
||||
/// </summary>
|
||||
public interface IQueryBreakdownRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new QueryBreakdown to the repository and saves changes.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The QueryBreakdown to add.</param>
|
||||
/// <returns>The ID of the added entity.</returns>
|
||||
Task<int> AddAsync(QueryBreakdown queryBreakdown);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a QueryBreakdown by ID and converts it from the entity.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the QueryBreakdown entity.</param>
|
||||
/// <returns>The QueryBreakdown, or null if not found.</returns>
|
||||
Task<QueryBreakdown?> GetByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a QueryBreakdownEntity by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the entity.</param>
|
||||
/// <returns>The QueryBreakdownEntity, or null if not found.</returns>
|
||||
Task<QueryBreakdownEntity?> GetEntityByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all QueryBreakdowns.
|
||||
/// </summary>
|
||||
/// <returns>A list of all QueryBreakdowns.</returns>
|
||||
Task<List<QueryBreakdown>> GetAllAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all QueryBreakdownEntities.
|
||||
/// </summary>
|
||||
/// <returns>A list of all QueryBreakdownEntities.</returns>
|
||||
Task<List<QueryBreakdownEntity>> GetAllEntitiesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing QueryBreakdown and saves changes.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the entity to update.</param>
|
||||
/// <param name="queryBreakdown">The updated QueryBreakdown.</param>
|
||||
Task UpdateAsync(int id, QueryBreakdown queryBreakdown);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a QueryBreakdown by ID and saves changes.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the entity to delete.</param>
|
||||
/// <returns>True if the entity was deleted; false if not found.</returns>
|
||||
Task<bool> DeleteAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of all QueryBreakdown entities.
|
||||
/// </summary>
|
||||
/// <returns>The count of entities.</returns>
|
||||
Task<int> GetCountAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of IQueryBreakdownRepository for managing QueryBreakdown entities in Entity Framework Core.
|
||||
/// </summary>
|
||||
public class QueryBreakdownRepository : IQueryBreakdownRepository
|
||||
{
|
||||
private readonly DbContext _context;
|
||||
private readonly IQueryBreakdownMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the QueryBreakdownRepository.
|
||||
/// </summary>
|
||||
/// <param name="context">The EF Core DbContext.</param>
|
||||
/// <param name="mapper">The mapper for converting between QueryBreakdown and QueryBreakdownEntity.</param>
|
||||
public QueryBreakdownRepository(DbContext context, IQueryBreakdownMapper mapper)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(mapper);
|
||||
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new QueryBreakdown to the repository and saves changes.
|
||||
/// </summary>
|
||||
public async Task<int> AddAsync(QueryBreakdown queryBreakdown)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queryBreakdown);
|
||||
|
||||
var (entity, parameters, withClauses) = _mapper.MapToEntityWithRelations(queryBreakdown);
|
||||
|
||||
// Add the main entity
|
||||
_context.Set<QueryBreakdownEntity>().Add(entity);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Add related entities with foreign key set
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
param.QueryBreakdownEntityId = entity.Id;
|
||||
_context.Set<QueryParameterEntity>().Add(param);
|
||||
}
|
||||
|
||||
foreach (var withClause in withClauses)
|
||||
{
|
||||
withClause.QueryBreakdownEntityId = entity.Id;
|
||||
_context.Set<WithClauseEntity>().Add(withClause);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return entity.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a QueryBreakdown by ID and converts it from the entity.
|
||||
/// </summary>
|
||||
public async Task<QueryBreakdown?> GetByIdAsync(int id)
|
||||
{
|
||||
var entity = await _context.Set<QueryBreakdownEntity>()
|
||||
.Include(e => e.Parameters)
|
||||
.Include(e => e.WithClauses)
|
||||
.FirstOrDefaultAsync(e => e.Id == id);
|
||||
|
||||
return entity != null ? _mapper.MapToDomainModelWithRelations(entity) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a QueryBreakdownEntity by ID.
|
||||
/// </summary>
|
||||
public async Task<QueryBreakdownEntity?> GetEntityByIdAsync(int id)
|
||||
{
|
||||
return await _context.Set<QueryBreakdownEntity>()
|
||||
.FirstOrDefaultAsync(e => e.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all QueryBreakdowns.
|
||||
/// </summary>
|
||||
public async Task<List<QueryBreakdown>> GetAllAsync()
|
||||
{
|
||||
var entities = await _context.Set<QueryBreakdownEntity>().ToListAsync();
|
||||
return entities.ConvertAll(e => _mapper.MapToDomainModel(e));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all QueryBreakdownEntities.
|
||||
/// </summary>
|
||||
public async Task<List<QueryBreakdownEntity>> GetAllEntitiesAsync()
|
||||
{
|
||||
return await _context.Set<QueryBreakdownEntity>().ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing QueryBreakdown and saves changes.
|
||||
/// </summary>
|
||||
public async Task UpdateAsync(int id, QueryBreakdown queryBreakdown)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queryBreakdown);
|
||||
|
||||
var entity = await _context.Set<QueryBreakdownEntity>().FirstOrDefaultAsync(e => e.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new InvalidOperationException($"QueryBreakdown with ID {id} not found.");
|
||||
}
|
||||
|
||||
var updatedEntity = _mapper.MapToEntity(queryBreakdown);
|
||||
|
||||
// Update the main entity
|
||||
entity.SelectClause = updatedEntity.SelectClause;
|
||||
entity.SelectClauseComment = updatedEntity.SelectClauseComment;
|
||||
entity.FromClause = updatedEntity.FromClause;
|
||||
entity.FromClauseComment = updatedEntity.FromClauseComment;
|
||||
entity.WhereClause = updatedEntity.WhereClause;
|
||||
entity.WhereClauseComment = updatedEntity.WhereClauseComment;
|
||||
entity.GroupByClause = updatedEntity.GroupByClause;
|
||||
entity.GroupByClauseComment = updatedEntity.GroupByClauseComment;
|
||||
entity.HavingClause = updatedEntity.HavingClause;
|
||||
entity.HavingClauseComment = updatedEntity.HavingClauseComment;
|
||||
entity.OrderByClause = updatedEntity.OrderByClause;
|
||||
entity.OrderByClauseComment = updatedEntity.OrderByClauseComment;
|
||||
entity.WithClause = updatedEntity.WithClause;
|
||||
entity.RawSql = updatedEntity.RawSql;
|
||||
entity.SetupClausesJson = updatedEntity.SetupClausesJson;
|
||||
entity.FinishClausesJson = updatedEntity.FinishClausesJson;
|
||||
entity.ParametersJson = updatedEntity.ParametersJson;
|
||||
entity.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Delete and recreate related entities
|
||||
var existingParameters = _context.Set<QueryParameterEntity>().Where(p => p.QueryBreakdownEntityId == id);
|
||||
_context.Set<QueryParameterEntity>().RemoveRange(existingParameters);
|
||||
|
||||
var existingWithClauses = _context.Set<WithClauseEntity>().Where(w => w.QueryBreakdownEntityId == id);
|
||||
_context.Set<WithClauseEntity>().RemoveRange(existingWithClauses);
|
||||
|
||||
var (_, parameters, withClauses) = _mapper.MapToEntityWithRelations(queryBreakdown);
|
||||
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
param.QueryBreakdownEntityId = id;
|
||||
_context.Set<QueryParameterEntity>().Add(param);
|
||||
}
|
||||
|
||||
foreach (var withClause in withClauses)
|
||||
{
|
||||
withClause.QueryBreakdownEntityId = id;
|
||||
_context.Set<WithClauseEntity>().Add(withClause);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a QueryBreakdown by ID and saves changes.
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
var entity = await _context.Set<QueryBreakdownEntity>().FirstOrDefaultAsync(e => e.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_context.Set<QueryBreakdownEntity>().Remove(entity);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of all QueryBreakdown entities.
|
||||
/// </summary>
|
||||
public async Task<int> GetCountAsync()
|
||||
{
|
||||
return await _context.Set<QueryBreakdownEntity>().CountAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.EFCore</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - EF Core</Product>
|
||||
<Description>Entity Framework Core integration and support for Strata.SqlTools QueryBreakdown functionality, allowing seamless mapping of SQL query breakdowns onto existing DbContext and database models.</Description>
|
||||
<PackageTags>sql;efcore;entity-framework;query-builder;database;orm</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with EF Core integration for QueryBreakdown functionality.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,281 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
using Strata.SqlTools.Comparers.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Analyzers.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Statistics about a collection of analyzed queries.
|
||||
/// </summary>
|
||||
public record QueryCollectionStatistics(
|
||||
int TotalQueries,
|
||||
int UniqueQueries,
|
||||
List<LinqQueryBreakdown> DuplicateQueries,
|
||||
Dictionary<string, int> TableUsageFrequency,
|
||||
Dictionary<string, int> ColumnSelectionFrequency,
|
||||
int QueriesWithoutWhere,
|
||||
int QueriesWithoutOrderBy,
|
||||
int QueriesWithSelectAll,
|
||||
double AverageComplexity,
|
||||
int ComplexQueriesCount
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the deduplication rate (unique queries / total queries).
|
||||
/// </summary>
|
||||
public double DeduplicationRate => TotalQueries == 0 ? 0.0 : (double)UniqueQueries / TotalQueries;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a formatted statistics report.
|
||||
/// </summary>
|
||||
public string GetReport()
|
||||
{
|
||||
var report = new System.Text.StringBuilder();
|
||||
report.AppendLine("Query Collection Analysis Report");
|
||||
report.AppendLine("================================");
|
||||
report.AppendLine($"Total Queries: {TotalQueries}");
|
||||
report.AppendLine($"Unique Queries: {UniqueQueries} ({DeduplicationRate * 100:F1}%)");
|
||||
report.AppendLine($"Duplicate Queries: {DuplicateQueries.Count}");
|
||||
report.AppendLine();
|
||||
|
||||
report.AppendLine("Query Characteristics:");
|
||||
report.AppendLine($" Queries without WHERE: {QueriesWithoutWhere}");
|
||||
report.AppendLine($" Queries without ORDER BY: {QueriesWithoutOrderBy}");
|
||||
report.AppendLine($" Queries with SELECT *: {QueriesWithSelectAll}");
|
||||
report.AppendLine();
|
||||
|
||||
report.AppendLine("Complexity Analysis:");
|
||||
report.AppendLine($" Average Complexity Level: {AverageComplexity:F2}");
|
||||
report.AppendLine($" Complex Queries: {ComplexQueriesCount}");
|
||||
report.AppendLine();
|
||||
|
||||
if (TableUsageFrequency.Count > 0)
|
||||
{
|
||||
report.AppendLine("Most Frequently Used Tables:");
|
||||
foreach (var kvp in TableUsageFrequency.OrderByDescending(x => x.Value).Take(5))
|
||||
{
|
||||
report.AppendLine($" {kvp.Key}: {kvp.Value} times");
|
||||
}
|
||||
report.AppendLine();
|
||||
}
|
||||
|
||||
if (ColumnSelectionFrequency.Count > 0)
|
||||
{
|
||||
report.AppendLine("Most Frequently Selected Columns:");
|
||||
foreach (var kvp in ColumnSelectionFrequency.OrderByDescending(x => x.Value).Take(5))
|
||||
{
|
||||
report.AppendLine($" {kvp.Key}: {kvp.Value} times");
|
||||
}
|
||||
}
|
||||
|
||||
return report.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a collection of LinqQueryBreakdown queries for patterns, duplicates, and statistics.
|
||||
/// </summary>
|
||||
public class QueryCollectionAnalyzer
|
||||
{
|
||||
private readonly List<LinqQueryBreakdown> _queries;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryCollectionAnalyzer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="queries">The queries to analyze.</param>
|
||||
public QueryCollectionAnalyzer(IEnumerable<LinqQueryBreakdown> queries)
|
||||
{
|
||||
_queries = queries?.ToList() ?? throw new ArgumentNullException(nameof(queries));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes the query collection and returns comprehensive statistics.
|
||||
/// </summary>
|
||||
/// <returns>Statistics about the query collection.</returns>
|
||||
public QueryCollectionStatistics Analyze()
|
||||
{
|
||||
if (_queries.Count == 0)
|
||||
{
|
||||
return new QueryCollectionStatistics(
|
||||
0, 0, new List<LinqQueryBreakdown>(),
|
||||
new Dictionary<string, int>(),
|
||||
new Dictionary<string, int>(),
|
||||
0, 0, 0, 0.0, 0);
|
||||
}
|
||||
|
||||
var duplicates = FindDuplicates();
|
||||
var uniqueCount = _queries.Count - duplicates.Count;
|
||||
var tableUsage = AnalyzeTableUsage();
|
||||
var columnUsage = AnalyzeColumnUsage();
|
||||
var queriesWithoutWhere = _queries.Count(q => string.IsNullOrWhiteSpace(q.WhereClause?.Clause));
|
||||
var queriesWithoutOrderBy = _queries.Count(q => string.IsNullOrWhiteSpace(q.OrderByClause?.Clause));
|
||||
var queriesWithSelectAll = _queries.Count(q =>
|
||||
q.SelectClause?.Clause?.Trim() == "*");
|
||||
var complexityScores = _queries.Select(q => GetComplexityScore(q)).ToList();
|
||||
var avgComplexity = complexityScores.Average();
|
||||
var complexQueries = complexityScores.Count(c => c >= 7);
|
||||
|
||||
return new QueryCollectionStatistics(
|
||||
_queries.Count,
|
||||
uniqueCount,
|
||||
duplicates,
|
||||
tableUsage,
|
||||
columnUsage,
|
||||
queriesWithoutWhere,
|
||||
queriesWithoutOrderBy,
|
||||
queriesWithSelectAll,
|
||||
avgComplexity,
|
||||
complexQueries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds duplicate queries in the collection.
|
||||
/// </summary>
|
||||
/// <returns>List of queries that are identical to another query in the collection.</returns>
|
||||
public List<LinqQueryBreakdown> FindDuplicates()
|
||||
{
|
||||
var duplicates = new List<LinqQueryBreakdown>();
|
||||
|
||||
for (int i = 0; i < _queries.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < _queries.Count; j++)
|
||||
{
|
||||
if (QueryComparator.AreQueriesIdentical(_queries[i], _queries[j]) && !duplicates.Contains(_queries[j]))
|
||||
{
|
||||
duplicates.Add(_queries[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds similar queries that are not identical but have high similarity.
|
||||
/// </summary>
|
||||
/// <param name="minimumSimilarity">Minimum similarity score (0.0-1.0).</param>
|
||||
/// <returns>Pairs of similar queries and their similarity scores.</returns>
|
||||
public List<(LinqQueryBreakdown Query1, LinqQueryBreakdown Query2, double Similarity)> FindSimilarQueries(double minimumSimilarity = 0.75)
|
||||
{
|
||||
var similarPairs = new List<(LinqQueryBreakdown, LinqQueryBreakdown, double)>();
|
||||
|
||||
for (int i = 0; i < _queries.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < _queries.Count; j++)
|
||||
{
|
||||
var similarity = QueryComparator.GetSimilarity(_queries[i], _queries[j]);
|
||||
if (similarity >= minimumSimilarity && similarity < 1.0)
|
||||
{
|
||||
similarPairs.Add((_queries[i], _queries[j], similarity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return similarPairs.OrderByDescending(x => x.Item3).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes table usage frequency across all queries.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary of table names and their usage counts.</returns>
|
||||
private Dictionary<string, int> AnalyzeTableUsage()
|
||||
{
|
||||
var tableUsage = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var query in _queries)
|
||||
{
|
||||
var table = query.FromClause?.Clause?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(table))
|
||||
{
|
||||
if (tableUsage.ContainsKey(table))
|
||||
{
|
||||
tableUsage[table]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
tableUsage[table] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tableUsage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes column selection frequency across all queries.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary of column names and their selection frequency.</returns>
|
||||
private Dictionary<string, int> AnalyzeColumnUsage()
|
||||
{
|
||||
var columnUsage = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var query in _queries)
|
||||
{
|
||||
var selectClause = query.SelectClause?.Clause;
|
||||
if (string.IsNullOrWhiteSpace(selectClause) || selectClause.Trim() == "*")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split columns and count them
|
||||
var columns = selectClause.Split(',');
|
||||
foreach (var col in columns)
|
||||
{
|
||||
var columnName = col.Trim();
|
||||
if (columnUsage.ContainsKey(columnName))
|
||||
{
|
||||
columnUsage[columnName]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
columnUsage[columnName] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return columnUsage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a complexity score for a query (0-10).
|
||||
/// </summary>
|
||||
private static int GetComplexityScore(LinqQueryBreakdown query)
|
||||
{
|
||||
int score = 1; // Base score for having a query
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.WhereClause?.Clause))
|
||||
{ score += 2; }
|
||||
if (!string.IsNullOrWhiteSpace(query.GroupByClause?.Clause))
|
||||
{ score += 2; }
|
||||
if (!string.IsNullOrWhiteSpace(query.HavingClause?.Clause))
|
||||
{ score += 2; }
|
||||
if (!string.IsNullOrWhiteSpace(query.OrderByClause?.Clause))
|
||||
{ score += 1; }
|
||||
|
||||
// Bonus points for complex WHERE conditions
|
||||
var whereClause = query.WhereClause?.Clause ?? string.Empty;
|
||||
var complexityIndicators = new[] { " AND ", " OR ", "IN (", "BETWEEN", "LIKE" };
|
||||
var complexParts = complexityIndicators.Count(ind => whereClause.Contains(ind, StringComparison.OrdinalIgnoreCase));
|
||||
score += Math.Min(complexParts, 2); // Cap at +2
|
||||
|
||||
return Math.Min(score, 10); // Cap at 10
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new analyzer for the given queries.
|
||||
/// </summary>
|
||||
/// <param name="queries">The queries to analyze.</param>
|
||||
/// <returns>A new QueryCollectionAnalyzer instance.</returns>
|
||||
public static QueryCollectionAnalyzer Analyze(IEnumerable<LinqQueryBreakdown> queries)
|
||||
{
|
||||
return new QueryCollectionAnalyzer(queries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary report for the query collection.
|
||||
/// </summary>
|
||||
/// <returns>A formatted analysis report.</returns>
|
||||
public string GetReport()
|
||||
{
|
||||
return Analyze().GetReport();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
using System.Linq.Expressions;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using PostgreSqlBreakdown = Strata.SqlTools.Breakdowns.PostgreSql.QueryBreakdown;
|
||||
using SnowflakeBreakdown = Strata.SqlTools.Breakdowns.Snowflake.QueryBreakdown;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a LINQ to SQL query breakdown, analyzing IQueryable expressions
|
||||
/// and converting them to SQL Server QueryBreakdown format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class analyzes LINQ expression trees to extract query components such as
|
||||
/// SELECT, WHERE, JOIN, GROUP BY, and ORDER BY clauses, making them accessible
|
||||
/// through the QueryBreakdown interface.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class LinqQueryBreakdown : QueryBreakdown
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the original LINQ expression that was analyzed.
|
||||
/// </summary>
|
||||
public Expression? OriginalExpression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the entity being queried.
|
||||
/// </summary>
|
||||
public Type? EntityType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this query uses LINQ method syntax.
|
||||
/// </summary>
|
||||
public bool IsMethodSyntax { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of LINQ method calls in the query chain.
|
||||
/// </summary>
|
||||
public List<string> MethodCallChain { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqQueryBreakdown"/> class.
|
||||
/// </summary>
|
||||
public LinqQueryBreakdown() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqQueryBreakdown"/> class with SELECT and FROM clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause (table name or data source).</param>
|
||||
public LinqQueryBreakdown(string selectClause, string fromClause) : base(selectClause, fromClause)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqQueryBreakdown"/> class with SELECT, FROM, and WHERE clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause (table name or data source).</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
public LinqQueryBreakdown(string selectClause, string fromClause, string whereClause)
|
||||
: base(selectClause, fromClause, whereClause)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes an IQueryable LINQ query and creates a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being queried.</typeparam>
|
||||
/// <param name="query">The IQueryable query to analyze.</param>
|
||||
/// <returns>A LinqQueryBreakdown representing the query structure.</returns>
|
||||
public static LinqQueryBreakdown Analyze<T>(IQueryable<T> query)
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
var breakdown = new LinqQueryBreakdown
|
||||
{
|
||||
OriginalExpression = query.Expression,
|
||||
EntityType = typeof(T)
|
||||
};
|
||||
|
||||
var visitor = new Visitors.LinqToSql.LinqExpressionVisitor();
|
||||
visitor.Visit(query.Expression);
|
||||
|
||||
// Extract components from visitor
|
||||
breakdown.SelectClause.Clause = visitor.SelectClause ?? "*";
|
||||
breakdown.FromClause.Clause = visitor.FromClause ?? typeof(T).Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(visitor.WhereClause))
|
||||
{
|
||||
breakdown.WhereClause.Clause = visitor.WhereClause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(visitor.OrderByClause))
|
||||
{
|
||||
breakdown.OrderByClause.Clause = visitor.OrderByClause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(visitor.GroupByClause))
|
||||
{
|
||||
breakdown.GroupByClause.Clause = visitor.GroupByClause;
|
||||
}
|
||||
|
||||
breakdown.MethodCallChain = visitor.MethodCallChain;
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to analyze an IQueryable LINQ query and create a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being queried.</typeparam>
|
||||
/// <param name="query">The IQueryable query to analyze.</param>
|
||||
/// <param name="result">The resulting LinqQueryBreakdown if successful.</param>
|
||||
/// <param name="errorMessage">Error message if analysis fails.</param>
|
||||
/// <returns>True if analysis succeeded; otherwise, false.</returns>
|
||||
public static bool TryAnalyze<T>(IQueryable<T> query, out LinqQueryBreakdown result, out string errorMessage)
|
||||
{
|
||||
result = new LinqQueryBreakdown();
|
||||
errorMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
result = Analyze(query);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary of the LINQ query structure.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the query composition.</returns>
|
||||
public string GetQuerySummary()
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectClause?.Clause))
|
||||
{
|
||||
parts.Add($"SELECT {SelectClause.Clause}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(FromClause?.Clause))
|
||||
{
|
||||
parts.Add($"FROM {FromClause.Clause}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(WhereClause?.Clause))
|
||||
{
|
||||
parts.Add($"WHERE {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(GroupByClause?.Clause))
|
||||
{
|
||||
parts.Add($"GROUP BY {GroupByClause.Clause}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(OrderByClause?.Clause))
|
||||
{
|
||||
parts.Add($"ORDER BY {OrderByClause.Clause}");
|
||||
}
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the LINQ method call chain as a string.
|
||||
/// </summary>
|
||||
/// <returns>A string representing the method chain.</returns>
|
||||
public string GetMethodChain()
|
||||
{
|
||||
if (MethodCallChain.Count == 0)
|
||||
{
|
||||
return "No method calls";
|
||||
}
|
||||
|
||||
return string.Join(" -> ", MethodCallChain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type for the query.</typeparam>
|
||||
/// <returns>An IQueryable of the specified type reconstructed from the breakdown, or null if the type doesn't match the original entity type.</returns>
|
||||
/// <remarks>
|
||||
/// This method attempts to reconstruct a LINQ query from the analyzed components (WHERE, ORDER BY, etc.).
|
||||
/// If a data source (IQueryable) is available in the breakdown's OriginalExpression, it will be used.
|
||||
/// Otherwise, returns null to indicate the query cannot be reconstructed without the original data source.
|
||||
/// </remarks>
|
||||
public override IQueryable<T>? GetQuery<T>() where T : class
|
||||
{
|
||||
// If we don't have the original expression, we cannot reconstruct the LINQ query
|
||||
if (OriginalExpression == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// The original expression is the full LINQ query that was analyzed
|
||||
// To use it, we need it to be an IQueryable<T>
|
||||
try
|
||||
{
|
||||
// If the original expression can be converted to IQueryable<T>, use it
|
||||
// Otherwise, we cannot safely reconstruct without the original query provider
|
||||
if (OriginalExpression is Expression expr && EntityType == typeof(T))
|
||||
{
|
||||
// We have the expression, but we don't have the provider to create IQueryable<T>
|
||||
// The breakdown analysis is one-way; reconstruction requires the original provider
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If any error occurs during reconstruction, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes an INSERT operation for the given entity.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being inserted.</typeparam>
|
||||
/// <param name="entity">The entity instance being inserted.</param>
|
||||
/// <returns>An InsertBreakdown representing the insert operation.</returns>
|
||||
public static Breakdowns.SqlServer.InsertBreakdown AnalyzeInsert<T>(T entity) where T : class
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entity));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.InsertBreakdown();
|
||||
breakdown.TableName.Clause = typeof(T).Name;
|
||||
|
||||
// Extract property names and values from entity
|
||||
var properties = typeof(T).GetProperties();
|
||||
var columnNames = new List<string>();
|
||||
var valuesList = new List<string>();
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var value = prop.GetValue(entity);
|
||||
columnNames.Add(prop.Name);
|
||||
valuesList.Add(value?.ToString() ?? "NULL");
|
||||
}
|
||||
|
||||
breakdown.InsertIntoClause.Clause = string.Join(", ", columnNames);
|
||||
breakdown.ValuesClause.Clause = string.Join(", ", valuesList);
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes an INSERT operation for multiple entities.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being inserted.</typeparam>
|
||||
/// <param name="entities">The entities being inserted.</param>
|
||||
/// <returns>An InsertBreakdown representing the bulk insert operation.</returns>
|
||||
public static Breakdowns.SqlServer.InsertBreakdown AnalyzeInsertRange<T>(IEnumerable<T> entities) where T : class
|
||||
{
|
||||
var entitiesList = entities?.ToList() ?? new List<T>();
|
||||
if (entitiesList.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Must provide at least one entity to insert.", nameof(entities));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.InsertBreakdown();
|
||||
breakdown.TableName.Clause = typeof(T).Name;
|
||||
|
||||
// Use first entity to get column names
|
||||
var firstEntity = entitiesList.First();
|
||||
var properties = typeof(T).GetProperties();
|
||||
var columnNames = new List<string>();
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
columnNames.Add(prop.Name);
|
||||
}
|
||||
|
||||
breakdown.InsertIntoClause.Clause = string.Join(", ", columnNames);
|
||||
|
||||
// Add values for each entity
|
||||
var allValues = new List<string>();
|
||||
foreach (var entity in entitiesList)
|
||||
{
|
||||
var rowValues = new List<string>();
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var value = prop.GetValue(entity);
|
||||
rowValues.Add(value?.ToString() ?? "NULL");
|
||||
}
|
||||
allValues.Add($"({string.Join(", ", rowValues)})");
|
||||
}
|
||||
|
||||
breakdown.ValuesClause.Clause = string.Join(", ", allValues);
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a DELETE operation based on a filter expression.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being deleted.</typeparam>
|
||||
/// <param name="filterExpression">The filter expression defining which entities to delete.</param>
|
||||
/// <returns>A DeleteBreakdown representing the delete operation.</returns>
|
||||
public static Breakdowns.SqlServer.DeleteBreakdown AnalyzeDelete<T>(Expression<Func<T, bool>> filterExpression) where T : class
|
||||
{
|
||||
if (filterExpression == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filterExpression));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.DeleteBreakdown();
|
||||
breakdown.FromClause.Clause = typeof(T).Name;
|
||||
|
||||
// Analyze the filter expression to extract WHERE clause
|
||||
var visitor = new Visitors.LinqToSql.LinqExpressionVisitor();
|
||||
visitor.Visit(filterExpression);
|
||||
|
||||
if (!string.IsNullOrEmpty(visitor.WhereClause))
|
||||
{
|
||||
breakdown.WhereClause.Clause = visitor.WhereClause;
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes an UPDATE operation based on filter and update expressions.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being updated.</typeparam>
|
||||
/// <param name="filterExpression">The filter expression defining which entities to update.</param>
|
||||
/// <param name="updateExpression">The update expression defining what to update.</param>
|
||||
/// <returns>An UpdateBreakdown representing the update operation.</returns>
|
||||
public static Breakdowns.SqlServer.UpdateBreakdown AnalyzeUpdate<T>(
|
||||
Expression<Func<T, bool>> filterExpression,
|
||||
Expression<Func<T, T>> updateExpression) where T : class
|
||||
{
|
||||
if (filterExpression == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filterExpression));
|
||||
}
|
||||
if (updateExpression == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(updateExpression));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.UpdateBreakdown();
|
||||
breakdown.TableName.Clause = typeof(T).Name;
|
||||
|
||||
// Analyze filter expression for WHERE clause
|
||||
var filterVisitor = new Visitors.LinqToSql.LinqExpressionVisitor();
|
||||
filterVisitor.Visit(filterExpression);
|
||||
|
||||
if (!string.IsNullOrEmpty(filterVisitor.WhereClause))
|
||||
{
|
||||
breakdown.WhereClause.Clause = filterVisitor.WhereClause;
|
||||
}
|
||||
|
||||
// For the SET clause, we collect property assignments
|
||||
var setClauseParts = new List<string>();
|
||||
if (updateExpression.Body is System.Linq.Expressions.NewExpression newExpr)
|
||||
{
|
||||
for (int i = 0; i < newExpr.Arguments.Count; i++)
|
||||
{
|
||||
var arg = newExpr.Arguments[i];
|
||||
var member = newExpr.Members?[i];
|
||||
if (member != null)
|
||||
{
|
||||
setClauseParts.Add($"{member.Name} = {arg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (setClauseParts.Count > 0)
|
||||
{
|
||||
breakdown.SetClause.Clause = string.Join(", ", setClauseParts);
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a procedure call breakdown.
|
||||
/// </summary>
|
||||
/// <param name="procedureName">The name of the stored procedure.</param>
|
||||
/// <param name="parameters">The procedure parameters.</param>
|
||||
/// <returns>A ProcedureBreakdown representing the procedure call.</returns>
|
||||
public static Breakdowns.SqlServer.ProcedureBreakdown AnalyzeProcedure(string procedureName, params object[] parameters)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(procedureName))
|
||||
{
|
||||
throw new ArgumentException("Procedure name cannot be null or empty.", nameof(procedureName));
|
||||
}
|
||||
|
||||
var breakdown = new Breakdowns.SqlServer.ProcedureBreakdown();
|
||||
breakdown.ProcedureName.Clause = procedureName;
|
||||
|
||||
if (parameters != null && parameters.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var paramName = $"@param{i}";
|
||||
var paramValue = parameters[i]?.ToString() ?? "NULL";
|
||||
breakdown.Parameters.Add(paramName, paramValue);
|
||||
}
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a query execution trace context.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type being traced.</typeparam>
|
||||
/// <param name="query">The query being traced.</param>
|
||||
/// <param name="executionContext">Additional execution context.</param>
|
||||
/// <returns>A string representation of the trace analysis.</returns>
|
||||
public static string AnalyzeTrace<T>(IQueryable<T> query, string? executionContext = null) where T : class
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"Trace Context for {typeof(T).Name}",
|
||||
$"Entity Type: {typeof(T).FullName}",
|
||||
$"Query Provider: {query.Provider?.GetType().Name ?? "Unknown"}",
|
||||
$"Expression: {query.Expression}"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(executionContext))
|
||||
{
|
||||
lines.Add($"Execution Context: {executionContext}");
|
||||
}
|
||||
|
||||
lines.Add($"Timestamp: {DateTime.UtcNow:O}");
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this LINQ breakdown to a SQL Server QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <returns>A SQL Server QueryBreakdown with the same clauses as this breakdown.</returns>
|
||||
public Strata.SqlTools.Breakdowns.SqlServer.QueryBreakdown ConvertToSqlServerBreakdown()
|
||||
{
|
||||
var sqlServerBreakdown = new Strata.SqlTools.Breakdowns.SqlServer.QueryBreakdown();
|
||||
|
||||
// Copy all clause information from this breakdown
|
||||
sqlServerBreakdown.SelectClause.Clause = SelectClause?.Clause;
|
||||
sqlServerBreakdown.SelectClause.Comment = SelectClause?.Comment;
|
||||
sqlServerBreakdown.FromClause.Clause = FromClause?.Clause;
|
||||
sqlServerBreakdown.FromClause.Comment = FromClause?.Comment;
|
||||
sqlServerBreakdown.WhereClause.Clause = WhereClause?.Clause;
|
||||
sqlServerBreakdown.WhereClause.Comment = WhereClause?.Comment;
|
||||
sqlServerBreakdown.GroupByClause.Clause = GroupByClause?.Clause;
|
||||
sqlServerBreakdown.GroupByClause.Comment = GroupByClause?.Comment;
|
||||
sqlServerBreakdown.HavingClause.Clause = HavingClause?.Clause;
|
||||
sqlServerBreakdown.HavingClause.Comment = HavingClause?.Comment;
|
||||
sqlServerBreakdown.OrderByClause.Clause = OrderByClause?.Clause;
|
||||
sqlServerBreakdown.OrderByClause.Comment = OrderByClause?.Comment;
|
||||
|
||||
return sqlServerBreakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this LINQ breakdown to a PostgreSQL QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <returns>A PostgreSQL QueryBreakdown with the same clauses as this breakdown.</returns>
|
||||
public PostgreSqlBreakdown ConvertToPostgreSqlBreakdown()
|
||||
{
|
||||
var postgresBreakdown = new PostgreSqlBreakdown();
|
||||
|
||||
// Copy all clause information from this breakdown
|
||||
postgresBreakdown.SelectClause.Clause = SelectClause?.Clause;
|
||||
postgresBreakdown.SelectClause.Comment = SelectClause?.Comment;
|
||||
postgresBreakdown.FromClause.Clause = FromClause?.Clause;
|
||||
postgresBreakdown.FromClause.Comment = FromClause?.Comment;
|
||||
postgresBreakdown.WhereClause.Clause = WhereClause?.Clause;
|
||||
postgresBreakdown.WhereClause.Comment = WhereClause?.Comment;
|
||||
postgresBreakdown.GroupByClause.Clause = GroupByClause?.Clause;
|
||||
postgresBreakdown.GroupByClause.Comment = GroupByClause?.Comment;
|
||||
postgresBreakdown.HavingClause.Clause = HavingClause?.Clause;
|
||||
postgresBreakdown.HavingClause.Comment = HavingClause?.Comment;
|
||||
postgresBreakdown.OrderByClause.Clause = OrderByClause?.Clause;
|
||||
postgresBreakdown.OrderByClause.Comment = OrderByClause?.Comment;
|
||||
|
||||
return postgresBreakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this LINQ breakdown to a Snowflake QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <returns>A Snowflake QueryBreakdown with the same clauses as this breakdown.</returns>
|
||||
public SnowflakeBreakdown ConvertToSnowflakeBreakdown()
|
||||
{
|
||||
var snowflakeBreakdown = new SnowflakeBreakdown();
|
||||
|
||||
// Copy all clause information from this breakdown
|
||||
snowflakeBreakdown.SelectClause.Clause = SelectClause?.Clause;
|
||||
snowflakeBreakdown.SelectClause.Comment = SelectClause?.Comment;
|
||||
snowflakeBreakdown.FromClause.Clause = FromClause?.Clause;
|
||||
snowflakeBreakdown.FromClause.Comment = FromClause?.Comment;
|
||||
snowflakeBreakdown.WhereClause.Clause = WhereClause?.Clause;
|
||||
snowflakeBreakdown.WhereClause.Comment = WhereClause?.Comment;
|
||||
snowflakeBreakdown.GroupByClause.Clause = GroupByClause?.Clause;
|
||||
snowflakeBreakdown.GroupByClause.Comment = GroupByClause?.Comment;
|
||||
snowflakeBreakdown.HavingClause.Clause = HavingClause?.Clause;
|
||||
snowflakeBreakdown.HavingClause.Comment = HavingClause?.Comment;
|
||||
snowflakeBreakdown.OrderByClause.Clause = OrderByClause?.Clause;
|
||||
snowflakeBreakdown.OrderByClause.Comment = OrderByClause?.Comment;
|
||||
|
||||
return snowflakeBreakdown;
|
||||
}
|
||||
|
||||
#region Dialect-Specific SQL Generation
|
||||
|
||||
/// <summary>
|
||||
/// Generates SQL Server T-SQL from this breakdown.
|
||||
/// </summary>
|
||||
/// <returns>SQL Server formatted SQL statement.</returns>
|
||||
public string ToSqlServerSql()
|
||||
{
|
||||
return ConvertToSqlServerBreakdown().GetSql();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates PostgreSQL SQL from this breakdown.
|
||||
/// </summary>
|
||||
/// <returns>PostgreSQL formatted SQL statement.</returns>
|
||||
public string ToPostgreSqlSql()
|
||||
{
|
||||
return ConvertToPostgreSqlBreakdown().GetSql();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates Snowflake SQL from this breakdown.
|
||||
/// </summary>
|
||||
/// <returns>Snowflake formatted SQL statement.</returns>
|
||||
public string ToSnowflakeSql()
|
||||
{
|
||||
return ConvertToSnowflakeBreakdown().GetSql();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Query Analysis and Validation
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this query has a WHERE clause for safe modification operations.
|
||||
/// </summary>
|
||||
/// <returns>True if WHERE clause exists; otherwise, false.</returns>
|
||||
public bool HasWhereClause()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(WhereClause?.Clause);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this query has GROUP BY clause.
|
||||
/// </summary>
|
||||
/// <returns>True if GROUP BY clause exists; otherwise, false.</returns>
|
||||
public bool HasGroupByClause()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(GroupByClause?.Clause);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this query selects all columns (SELECT *).
|
||||
/// </summary>
|
||||
/// <returns>True if SELECT contains *; otherwise, false.</returns>
|
||||
public bool SelectsAllColumns()
|
||||
{
|
||||
return SelectClause?.Clause?.Contains("*") ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets query complexity estimate based on clause count.
|
||||
/// </summary>
|
||||
/// <returns>Complexity level: Simple, Moderate, or Complex.</returns>
|
||||
public string GetComplexityLevel()
|
||||
{
|
||||
var clauseCount = 0;
|
||||
if (HasWhereClause())
|
||||
{
|
||||
clauseCount++;
|
||||
}
|
||||
if (HasGroupByClause())
|
||||
{
|
||||
clauseCount++;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(HavingClause?.Clause))
|
||||
{
|
||||
clauseCount++;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(OrderByClause?.Clause))
|
||||
{
|
||||
clauseCount++;
|
||||
}
|
||||
|
||||
return clauseCount switch
|
||||
{
|
||||
0 => "Simple",
|
||||
1 or 2 => "Moderate",
|
||||
_ => "Complex"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a detailed natural language explanation of what this query does.
|
||||
/// </summary>
|
||||
/// <returns>Human-readable query explanation.</returns>
|
||||
public string GetDetailedExplanation()
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
// Basic query structure
|
||||
if (!string.IsNullOrWhiteSpace(SelectClause?.Clause))
|
||||
{
|
||||
var what = SelectsAllColumns() ? "all columns" : "specific columns";
|
||||
lines.Add($"This query selects {what}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FromClause?.Clause))
|
||||
{
|
||||
lines.Add($"from the {FromClause.Clause} table");
|
||||
}
|
||||
|
||||
// Filtering
|
||||
if (HasWhereClause())
|
||||
{
|
||||
lines.Add($"where {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
// Grouping
|
||||
if (HasGroupByClause())
|
||||
{
|
||||
lines.Add($"grouped by {GroupByClause.Clause}");
|
||||
}
|
||||
|
||||
// Filtering grouped results
|
||||
if (!string.IsNullOrWhiteSpace(HavingClause?.Clause))
|
||||
{
|
||||
lines.Add($"with groups filtered where {HavingClause.Clause}");
|
||||
}
|
||||
|
||||
// Sorting
|
||||
if (!string.IsNullOrWhiteSpace(OrderByClause?.Clause))
|
||||
{
|
||||
lines.Add($"sorted by {OrderByClause.Clause}");
|
||||
}
|
||||
|
||||
// Complexity note
|
||||
var complexity = GetComplexityLevel();
|
||||
if (complexity != "Simple")
|
||||
{
|
||||
lines.Add($"(Complexity: {complexity})");
|
||||
}
|
||||
|
||||
return string.Join(" ", lines);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Builders.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Fluent builder for constructing LinqQueryBreakdown instances programmatically.
|
||||
/// Useful for scenarios where you don't have a live IQueryable to analyze.
|
||||
/// </summary>
|
||||
public class LinqQueryBreakdownBuilder
|
||||
{
|
||||
private readonly LinqQueryBreakdown _breakdown;
|
||||
private readonly List<string> _selectColumns = new();
|
||||
private string? _fromTable;
|
||||
private string? _whereClause;
|
||||
private string? _groupByClause;
|
||||
private string? _havingClause;
|
||||
private string? _orderByClause;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqQueryBreakdownBuilder"/> class.
|
||||
/// </summary>
|
||||
public LinqQueryBreakdownBuilder()
|
||||
{
|
||||
_breakdown = new LinqQueryBreakdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the SELECT columns for the query.
|
||||
/// </summary>
|
||||
/// <param name="columns">Column names to select.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder SelectColumns(params string[] columns)
|
||||
{
|
||||
if (columns.Length == 0)
|
||||
{
|
||||
_selectColumns.Add("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectColumns.AddRange(columns);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the SELECT to all columns (*).
|
||||
/// </summary>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder SelectAll()
|
||||
{
|
||||
_selectColumns.Clear();
|
||||
_selectColumns.Add("*");
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the FROM table for the query.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder FromTable(string tableName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tableName))
|
||||
{
|
||||
throw new ArgumentException("Table name cannot be null or empty.", nameof(tableName));
|
||||
}
|
||||
_fromTable = tableName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the WHERE clause for the query.
|
||||
/// </summary>
|
||||
/// <param name="condition">The WHERE condition.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder Where(string condition)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(condition))
|
||||
{
|
||||
_whereClause = condition;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the GROUP BY clause for the query.
|
||||
/// </summary>
|
||||
/// <param name="columns">The columns to group by.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder GroupBy(params string[] columns)
|
||||
{
|
||||
if (columns.Length > 0)
|
||||
{
|
||||
_groupByClause = string.Join(", ", columns);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the HAVING clause for the query.
|
||||
/// </summary>
|
||||
/// <param name="condition">The HAVING condition.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder Having(string condition)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(condition))
|
||||
{
|
||||
_havingClause = condition;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the ORDER BY clause for the query.
|
||||
/// </summary>
|
||||
/// <param name="orderSpecification">The ORDER BY specification (e.g., "Name ASC, Age DESC").</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder OrderBy(string orderSpecification)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(orderSpecification))
|
||||
{
|
||||
_orderByClause = orderSpecification;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an ORDER BY clause in ascending order.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to order by.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder OrderByAscending(string column)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(column))
|
||||
{
|
||||
throw new ArgumentException("Column name cannot be null or empty.", nameof(column));
|
||||
}
|
||||
_orderByClause = $"{column} ASC";
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an ORDER BY clause in descending order.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to order by.</param>
|
||||
/// <returns>This builder for method chaining.</returns>
|
||||
public LinqQueryBreakdownBuilder OrderByDescending(string column)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(column))
|
||||
{
|
||||
throw new ArgumentException("Column name cannot be null or empty.", nameof(column));
|
||||
}
|
||||
_orderByClause = $"{column} DESC";
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and returns the LinqQueryBreakdown instance.
|
||||
/// </summary>
|
||||
/// <returns>A new LinqQueryBreakdown with the configured clauses.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when required clauses are missing.</exception>
|
||||
public LinqQueryBreakdown Build()
|
||||
{
|
||||
if (_selectColumns.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("At least one SELECT column must be specified.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_fromTable))
|
||||
{
|
||||
throw new InvalidOperationException("FROM table must be specified.");
|
||||
}
|
||||
|
||||
var breakdown = new LinqQueryBreakdown(
|
||||
string.Join(", ", _selectColumns),
|
||||
_fromTable,
|
||||
_whereClause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_groupByClause))
|
||||
{
|
||||
breakdown.GroupByClause.Clause = _groupByClause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_havingClause))
|
||||
{
|
||||
breakdown.HavingClause.Clause = _havingClause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_orderByClause))
|
||||
{
|
||||
breakdown.OrderByClause.Clause = _orderByClause;
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a SQL Server formatted preview of the query being built.
|
||||
/// </summary>
|
||||
/// <returns>Preview SQL statement.</returns>
|
||||
public string PreviewSql()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Build().ToSqlServerSql();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return "-- Incomplete query (missing required clauses)";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new builder with the default state.
|
||||
/// </summary>
|
||||
/// <returns>A new LinqQueryBreakdownBuilder instance.</returns>
|
||||
public static LinqQueryBreakdownBuilder Create()
|
||||
{
|
||||
return new LinqQueryBreakdownBuilder();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a builder with a table already specified.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table to select from.</param>
|
||||
/// <returns>A new builder with the table set.</returns>
|
||||
public static LinqQueryBreakdownBuilder CreateForTable(string tableName)
|
||||
{
|
||||
return new LinqQueryBreakdownBuilder().FromTable(tableName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Comparers.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Result of comparing two LinqQueryBreakdown instances.
|
||||
/// </summary>
|
||||
public record QueryComparisonResult(
|
||||
bool AreEquivalent,
|
||||
double SimilarityScore, // 0.0 to 1.0
|
||||
List<string> Differences,
|
||||
bool HaveSameSelectColumns,
|
||||
bool HaveSameFromTable,
|
||||
bool HaveSameWhereClause,
|
||||
bool HaveSameGroupBy,
|
||||
bool HaveSameHaving,
|
||||
bool HaveSameOrderBy
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a formatted comparison report.
|
||||
/// </summary>
|
||||
public string GetReport()
|
||||
{
|
||||
var report = new System.Text.StringBuilder();
|
||||
report.AppendLine($"Query Comparison Report");
|
||||
report.AppendLine($"Similarity: {(SimilarityScore * 100):F1}%");
|
||||
report.AppendLine($"Equivalent: {(AreEquivalent ? "Yes" : "No")}");
|
||||
report.AppendLine();
|
||||
|
||||
if (Differences.Count == 0)
|
||||
{
|
||||
report.AppendLine("✓ Queries are identical");
|
||||
return report.ToString();
|
||||
}
|
||||
|
||||
report.AppendLine("Differences:");
|
||||
foreach (var diff in Differences)
|
||||
{
|
||||
report.AppendLine($" • {diff}");
|
||||
}
|
||||
|
||||
return report.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares LinqQueryBreakdown instances to detect similarity, equivalence, and duplicates.
|
||||
/// </summary>
|
||||
public class QueryComparator
|
||||
{
|
||||
private readonly LinqQueryBreakdown _query1;
|
||||
private readonly LinqQueryBreakdown _query2;
|
||||
private QueryComparisonResult? _result;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryComparator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query to compare.</param>
|
||||
/// <param name="query2">The second query to compare.</param>
|
||||
public QueryComparator(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
_query1 = query1 ?? throw new ArgumentNullException(nameof(query1));
|
||||
_query2 = query2 ?? throw new ArgumentNullException(nameof(query2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the comparison result, calculating it if needed.
|
||||
/// </summary>
|
||||
public QueryComparisonResult Result =>
|
||||
_result ??= PerformComparison();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the queries are equivalent (same structure).
|
||||
/// </summary>
|
||||
public bool AreEquivalent => Result.AreEquivalent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the similarity score from 0.0 (completely different) to 1.0 (identical).
|
||||
/// </summary>
|
||||
public double SimilarityScore => Result.SimilarityScore;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of differences found between the queries.
|
||||
/// </summary>
|
||||
public List<string> Differences => Result.Differences;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the actual comparison between the two queries.
|
||||
/// </summary>
|
||||
/// <returns>The comparison result.</returns>
|
||||
private QueryComparisonResult PerformComparison()
|
||||
{
|
||||
var differences = new List<string>();
|
||||
var scoreComponents = 0;
|
||||
var scoreMatches = 0;
|
||||
|
||||
// Compare SELECT clause
|
||||
var selectMatch = CompareSelectClauses(_query1, _query2);
|
||||
if (!selectMatch)
|
||||
{
|
||||
differences.Add("SELECT clauses differ");
|
||||
}
|
||||
scoreComponents++;
|
||||
if (selectMatch)
|
||||
{ scoreMatches++; }
|
||||
|
||||
// Compare FROM clause
|
||||
var fromMatch = CompareFromClauses(_query1, _query2);
|
||||
if (!fromMatch)
|
||||
{
|
||||
differences.Add("FROM clauses differ");
|
||||
}
|
||||
scoreComponents++;
|
||||
if (fromMatch)
|
||||
{ scoreMatches++; }
|
||||
|
||||
// Compare WHERE clause
|
||||
var whereMatch = CompareWhereClauses(_query1, _query2);
|
||||
if (!whereMatch)
|
||||
{
|
||||
differences.Add("WHERE clauses differ");
|
||||
}
|
||||
scoreComponents++;
|
||||
if (whereMatch)
|
||||
{ scoreMatches++; }
|
||||
|
||||
// Compare GROUP BY clause
|
||||
var groupByMatch = CompareGroupByClauses(_query1, _query2);
|
||||
if (!groupByMatch)
|
||||
{
|
||||
differences.Add("GROUP BY clauses differ");
|
||||
}
|
||||
scoreComponents++;
|
||||
if (groupByMatch)
|
||||
{ scoreMatches++; }
|
||||
|
||||
// Compare HAVING clause
|
||||
var havingMatch = CompareHavingClauses(_query1, _query2);
|
||||
if (!havingMatch)
|
||||
{
|
||||
differences.Add("HAVING clauses differ");
|
||||
}
|
||||
scoreComponents++;
|
||||
if (havingMatch)
|
||||
{ scoreMatches++; }
|
||||
|
||||
// Compare ORDER BY clause
|
||||
var orderByMatch = CompareOrderByClauses(_query1, _query2);
|
||||
if (!orderByMatch)
|
||||
{
|
||||
differences.Add("ORDER BY clauses differ");
|
||||
}
|
||||
scoreComponents++;
|
||||
if (orderByMatch)
|
||||
{ scoreMatches++; }
|
||||
|
||||
var similarityScore = scoreComponents > 0 ? (double)scoreMatches / scoreComponents : 0.0;
|
||||
var areEquivalent = differences.Count == 0;
|
||||
|
||||
return new QueryComparisonResult(
|
||||
areEquivalent,
|
||||
similarityScore,
|
||||
differences,
|
||||
selectMatch,
|
||||
fromMatch,
|
||||
whereMatch,
|
||||
groupByMatch,
|
||||
havingMatch,
|
||||
orderByMatch);
|
||||
}
|
||||
|
||||
private static bool CompareSelectClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
var select1 = NormalizeClause(query1.SelectClause?.Clause ?? string.Empty);
|
||||
var select2 = NormalizeClause(query2.SelectClause?.Clause ?? string.Empty);
|
||||
return StringEquals(select1, select2);
|
||||
}
|
||||
|
||||
private static bool CompareFromClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
var from1 = NormalizeClause(query1.FromClause?.Clause ?? string.Empty);
|
||||
var from2 = NormalizeClause(query2.FromClause?.Clause ?? string.Empty);
|
||||
return StringEquals(from1, from2);
|
||||
}
|
||||
|
||||
private static bool CompareWhereClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
var where1 = NormalizeClause(query1.WhereClause?.Clause ?? string.Empty);
|
||||
var where2 = NormalizeClause(query2.WhereClause?.Clause ?? string.Empty);
|
||||
return StringEquals(where1, where2);
|
||||
}
|
||||
|
||||
private static bool CompareGroupByClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
var groupBy1 = NormalizeClause(query1.GroupByClause?.Clause ?? string.Empty);
|
||||
var groupBy2 = NormalizeClause(query2.GroupByClause?.Clause ?? string.Empty);
|
||||
return StringEquals(groupBy1, groupBy2);
|
||||
}
|
||||
|
||||
private static bool CompareHavingClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
var having1 = NormalizeClause(query1.HavingClause?.Clause ?? string.Empty);
|
||||
var having2 = NormalizeClause(query2.HavingClause?.Clause ?? string.Empty);
|
||||
return StringEquals(having1, having2);
|
||||
}
|
||||
|
||||
private static bool CompareOrderByClauses(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
var orderBy1 = NormalizeClause(query1.OrderByClause?.Clause ?? string.Empty);
|
||||
var orderBy2 = NormalizeClause(query2.OrderByClause?.Clause ?? string.Empty);
|
||||
return StringEquals(orderBy1, orderBy2);
|
||||
}
|
||||
|
||||
private static string NormalizeClause(string clause)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clause))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Normalize whitespace and case
|
||||
return System.Text.RegularExpressions.Regex
|
||||
.Replace(clause.Trim(), @"\s+", " ")
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private static bool StringEquals(string? str1, string? str2)
|
||||
{
|
||||
return string.Equals(str1, str2, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new comparator for two queries.
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query.</param>
|
||||
/// <param name="query2">The second query.</param>
|
||||
/// <returns>A new QueryComparator instance.</returns>
|
||||
public static QueryComparator Compare(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
return new QueryComparator(query1, query2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if two queries are equivalent.
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query.</param>
|
||||
/// <param name="query2">The second query.</param>
|
||||
/// <returns>True if the queries are equivalent; otherwise, false.</returns>
|
||||
public static bool AreQueriesEquivalent(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
return new QueryComparator(query1, query2).AreEquivalent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if two queries are identical (same text after normalization).
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query.</param>
|
||||
/// <param name="query2">The second query.</param>
|
||||
/// <returns>True if the queries are identical; otherwise, false.</returns>
|
||||
public static bool AreQueriesIdentical(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
var comparator = new QueryComparator(query1, query2);
|
||||
return comparator.SimilarityScore >= 1.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the similarity score between two queries (0.0 to 1.0).
|
||||
/// </summary>
|
||||
/// <param name="query1">The first query.</param>
|
||||
/// <param name="query2">The second query.</param>
|
||||
/// <returns>A similarity score from 0.0 (completely different) to 1.0 (identical).</returns>
|
||||
public static double GetSimilarity(LinqQueryBreakdown query1, LinqQueryBreakdown query2)
|
||||
{
|
||||
return new QueryComparator(query1, query2).SimilarityScore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using PostgreSqlBreakdown = Strata.SqlTools.Breakdowns.PostgreSql.QueryBreakdown;
|
||||
using SnowflakeBreakdown = Strata.SqlTools.Breakdowns.Snowflake.QueryBreakdown;
|
||||
|
||||
namespace Strata.SqlTools.Converters.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Converts dialect-specific QueryBreakdown instances back to the generic LinqQueryBreakdown format.
|
||||
/// Enables parsing from any dialect and converting between all supported dialects.
|
||||
/// </summary>
|
||||
public static class ReverseConverterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a SQL Server QueryBreakdown to a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The SQL Server breakdown to convert.</param>
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this QueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
breakdown.WhereClause?.Clause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause))
|
||||
{
|
||||
linq.GroupByClause.Clause = breakdown.GroupByClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause))
|
||||
{
|
||||
linq.HavingClause.Clause = breakdown.HavingClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause))
|
||||
{
|
||||
linq.OrderByClause.Clause = breakdown.OrderByClause.Clause;
|
||||
}
|
||||
|
||||
return linq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PostgreSQL QueryBreakdown to a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The PostgreSQL breakdown to convert.</param>
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this PostgreSqlBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
breakdown.WhereClause?.Clause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause))
|
||||
{
|
||||
linq.GroupByClause.Clause = breakdown.GroupByClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause))
|
||||
{
|
||||
linq.HavingClause.Clause = breakdown.HavingClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause))
|
||||
{
|
||||
linq.OrderByClause.Clause = breakdown.OrderByClause.Clause;
|
||||
}
|
||||
|
||||
return linq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Snowflake QueryBreakdown to a LinqQueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The Snowflake breakdown to convert.</param>
|
||||
/// <returns>A new LinqQueryBreakdown with the same clauses.</returns>
|
||||
public static LinqQueryBreakdown ToLinqQueryBreakdown(this SnowflakeBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
var linq = new LinqQueryBreakdown(
|
||||
breakdown.SelectClause?.Clause ?? "*",
|
||||
breakdown.FromClause?.Clause ?? string.Empty,
|
||||
breakdown.WhereClause?.Clause ?? string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause))
|
||||
{
|
||||
linq.GroupByClause.Clause = breakdown.GroupByClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause))
|
||||
{
|
||||
linq.HavingClause.Clause = breakdown.HavingClause.Clause;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause))
|
||||
{
|
||||
linq.OrderByClause.Clause = breakdown.OrderByClause.Clause;
|
||||
}
|
||||
|
||||
return linq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a SQL Server QueryBreakdown to a different dialect.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The SQL Server breakdown to convert.</param>
|
||||
/// <param name="targetDialect">The target dialect: "postgresql", "snowflake", or "linq".</param>
|
||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||
public static object ConvertToDialect(this QueryBreakdown breakdown, string targetDialect)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
"postgresql" or "postgres" => breakdown.ToLinqQueryBreakdown().ConvertToPostgreSqlBreakdown(),
|
||||
"snowflake" => breakdown.ToLinqQueryBreakdown().ConvertToSnowflakeBreakdown(),
|
||||
"linq" => breakdown.ToLinqQueryBreakdown(),
|
||||
"sqlserver" or "sql_server" => breakdown,
|
||||
_ => throw new ArgumentException($"Unknown target dialect: {targetDialect}", nameof(targetDialect))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PostgreSQL QueryBreakdown to a different dialect.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The PostgreSQL breakdown to convert.</param>
|
||||
/// <param name="targetDialect">The target dialect: "sqlserver", "snowflake", or "linq".</param>
|
||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||
public static object ConvertToDialect(this PostgreSqlBreakdown breakdown, string targetDialect)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
"sqlserver" or "sql_server" => breakdown.ToLinqQueryBreakdown().ConvertToSqlServerBreakdown(),
|
||||
"snowflake" => breakdown.ToLinqQueryBreakdown().ConvertToSnowflakeBreakdown(),
|
||||
"linq" => breakdown.ToLinqQueryBreakdown(),
|
||||
"postgresql" or "postgres" => breakdown,
|
||||
_ => throw new ArgumentException($"Unknown target dialect: {targetDialect}", nameof(targetDialect))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Snowflake QueryBreakdown to a different dialect.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The Snowflake breakdown to convert.</param>
|
||||
/// <param name="targetDialect">The target dialect: "sqlserver", "postgresql", or "linq".</param>
|
||||
/// <returns>A new breakdown in the target dialect format.</returns>
|
||||
public static object ConvertToDialect(this SnowflakeBreakdown breakdown, string targetDialect)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
return targetDialect.ToLowerInvariant() switch
|
||||
{
|
||||
"sqlserver" or "sql_server" => breakdown.ToLinqQueryBreakdown().ConvertToSqlServerBreakdown(),
|
||||
"postgresql" or "postgres" => breakdown.ToLinqQueryBreakdown().ConvertToPostgreSqlBreakdown(),
|
||||
"linq" => breakdown.ToLinqQueryBreakdown(),
|
||||
"snowflake" => breakdown,
|
||||
_ => throw new ArgumentException($"Unknown target dialect: {targetDialect}", nameof(targetDialect))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# Strata.SqlTools.LinqToSql
|
||||
|
||||
LINQ to SQL support for Strata SQL Utilities, providing query breakdown and analysis capabilities for LINQ to SQL queries.
|
||||
|
||||
## Overview
|
||||
|
||||
This library extends Strata.SqlTools to work with LINQ to SQL queries, allowing you to:
|
||||
|
||||
- Analyze LINQ query expressions
|
||||
- Break down LINQ queries into their component parts
|
||||
- Convert LINQ expressions to QueryBreakdown objects
|
||||
- Generate SQL representations from LINQ queries
|
||||
- Visualize LINQ query structure
|
||||
|
||||
## Features
|
||||
|
||||
### LINQ Query Analysis
|
||||
- Extract SELECT, WHERE, JOIN, GROUP BY, and ORDER BY operations from LINQ expressions
|
||||
- Identify data sources and table references
|
||||
- Analyze query composition and complexity
|
||||
|
||||
### QueryBreakdown Integration
|
||||
- Convert LINQ `IQueryable<T>` to `QueryBreakdown` objects
|
||||
- Support for common LINQ methods: `Where`, `Select`, `OrderBy`, `GroupBy`, `Join`, etc.
|
||||
- Parameter extraction and analysis
|
||||
|
||||
### Expression Visitors
|
||||
- Custom expression visitors for LINQ expression trees
|
||||
- Support for method call expressions, lambda expressions, and member access
|
||||
- Handles both query syntax and method syntax
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
dotnet add package Strata.SqlTools.LinqToSql
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Query Breakdown
|
||||
|
||||
```csharp
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
using System.Linq;
|
||||
|
||||
// Your LINQ to SQL query
|
||||
var query = from user in context.Users
|
||||
where user.Age > 21
|
||||
orderby user.Name
|
||||
select new { user.Id, user.Name, user.Email };
|
||||
|
||||
// Analyze the query
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
|
||||
// Access breakdown components
|
||||
Console.WriteLine($"Select: {breakdown.SelectClause}");
|
||||
Console.WriteLine($"From: {breakdown.FromClause}");
|
||||
Console.WriteLine($"Where: {breakdown.WhereClause}");
|
||||
Console.WriteLine($"OrderBy: {breakdown.OrderByClause}");
|
||||
```
|
||||
|
||||
### Expression Analysis
|
||||
|
||||
```csharp
|
||||
using Strata.SqlTools.Visitors.LinqToSql;
|
||||
|
||||
// Analyze a specific expression
|
||||
Expression<Func<User, bool>> predicate = u => u.Age > 21 && u.Status == "Active";
|
||||
|
||||
var visitor = new LinqExpressionVisitor();
|
||||
visitor.Visit(predicate);
|
||||
|
||||
// Get analysis results
|
||||
var conditions = visitor.GetConditions();
|
||||
var parameters = visitor.GetParameters();
|
||||
```
|
||||
|
||||
### SQL Generation
|
||||
|
||||
```csharp
|
||||
// Generate SQL from LINQ query
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
string sql = breakdown.GetSql();
|
||||
|
||||
Console.WriteLine(sql);
|
||||
// Output: SELECT u.Id, u.Name, u.Email FROM Users u WHERE u.Age > 21 ORDER BY u.Name
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Components
|
||||
|
||||
- **LinqQueryBreakdown**: Main class for analyzing LINQ queries and converting them to breakdown format
|
||||
- **LinqExpressionVisitor**: Expression visitor for traversing LINQ expression trees
|
||||
- **LinqToSqlConverter**: Converts LINQ expressions to SQL Server QueryBreakdown objects
|
||||
|
||||
### Supported LINQ Methods
|
||||
|
||||
- `Where` → WHERE clause
|
||||
- `Select` → SELECT clause
|
||||
- `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending` → ORDER BY clause
|
||||
- `GroupBy` → GROUP BY clause
|
||||
- `Join`, `GroupJoin` → JOIN clauses
|
||||
- `First`, `FirstOrDefault`, `Single`, `SingleOrDefault` → TOP 1
|
||||
- `Take`, `Skip` → TOP n / OFFSET-FETCH
|
||||
- `Distinct` → DISTINCT
|
||||
- `Count`, `Sum`, `Average`, `Min`, `Max` → Aggregate functions
|
||||
|
||||
## Limitations
|
||||
|
||||
- LINQ to SQL translates to SQL Server T-SQL dialect
|
||||
- Complex expressions may not be fully analyzed
|
||||
- Some LINQ features may not have direct SQL equivalents
|
||||
- Requires the query to be `IQueryable<T>` (not `IEnumerable<T>`)
|
||||
|
||||
## Integration with Markdown
|
||||
|
||||
Use with `Strata.SqlTools.Markdown` to generate visual diagrams:
|
||||
|
||||
```csharp
|
||||
using Strata.SqlTools.Markdown.LinqToSql;
|
||||
|
||||
var breakdown = LinqQueryBreakdown.Analyze(query);
|
||||
var generator = new QueryBreakdownGenerator();
|
||||
|
||||
string mermaidDiagram = generator.GenerateMermaidDiagram(breakdown, "User Query");
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Strata.SqlTools](../Strata.SqlTools/README.md) - Core SQL utilities
|
||||
- [Strata.SqlTools.SqlServer](../Strata.SqlTools.SqlServer/README.md) - SQL Server support
|
||||
- [Strata.SqlTools.Markdown](../Strata.SqlTools.Markdown/README.md) - Markdown generation
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Copyright © Strata Decision Technology 2024-2026
|
||||
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.LinqToSql</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - LINQ to SQL</Product>
|
||||
<Description>LINQ to SQL specific implementations for Strata.SqlTools, including LINQ expression analysis, query breakdown, and SQL generation from LINQ queries. Provides tools to analyze and visualize LINQ to SQL query structures.</Description>
|
||||
<PackageTags>linq;linq-to-sql;sql;query-builder;expression-trees;database;dotnet</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with LINQ to SQL query analysis, breakdown, and visualization support.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.PostgreSql\Strata.SqlTools.PostgreSql.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.Snowflake\Strata.SqlTools.Snowflake.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- No additional package references needed - works with System.Linq.Expressions from .NET -->
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,311 @@
|
||||
using System.Collections.Immutable;
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Validators.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Severity level for validation issues.
|
||||
/// </summary>
|
||||
public enum ValidationSeverity
|
||||
{
|
||||
/// <summary>Informational message, no action required.</summary>
|
||||
Info = 0,
|
||||
|
||||
/// <summary>Warning - potential issue that should be reviewed.</summary>
|
||||
Warning = 1,
|
||||
|
||||
/// <summary>Error - definite issue that should be fixed.</summary>
|
||||
Error = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single validation issue found in a query.
|
||||
/// </summary>
|
||||
public record QueryValidationIssue(
|
||||
ValidationSeverity Severity,
|
||||
string Code,
|
||||
string Message,
|
||||
string? Details = null
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a formatted string representation of the validation issue.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var result = $"[{Severity}] {Code}: {Message}";
|
||||
if (!string.IsNullOrWhiteSpace(Details))
|
||||
{
|
||||
result += $" - {Details}";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates LinqQueryBreakdown instances and detects common anti-patterns.
|
||||
/// </summary>
|
||||
public class QueryValidator
|
||||
{
|
||||
private readonly List<QueryValidationIssue> _issues = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of validation issues found.
|
||||
/// </summary>
|
||||
public IReadOnlyList<QueryValidationIssue> Issues => _issues.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any errors were found.
|
||||
/// </summary>
|
||||
public bool HasErrors => _issues.Any(i => i.Severity == ValidationSeverity.Error);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any warnings were found.
|
||||
/// </summary>
|
||||
public bool HasWarnings => _issues.Any(i => i.Severity == ValidationSeverity.Warning);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a LinqQueryBreakdown instance and returns the result.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The breakdown to validate.</param>
|
||||
/// <returns>This validator for method chaining.</returns>
|
||||
public QueryValidator Validate(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(breakdown));
|
||||
}
|
||||
|
||||
_issues.Clear();
|
||||
|
||||
ValidateSelectClause(breakdown);
|
||||
ValidateFromClause(breakdown);
|
||||
ValidateWhereClause(breakdown);
|
||||
ValidateGroupByClause(breakdown);
|
||||
ValidateHavingClause(breakdown);
|
||||
ValidateOrderByClause(breakdown);
|
||||
ValidateCommonAntiPatterns(breakdown);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom validation issue.
|
||||
/// </summary>
|
||||
/// <param name="severity">The severity level.</param>
|
||||
/// <param name="code">The issue code (e.g., "RULE_001").</param>
|
||||
/// <param name="message">The issue message.</param>
|
||||
/// <param name="details">Optional detailed information.</param>
|
||||
/// <returns>This validator for method chaining.</returns>
|
||||
public QueryValidator AddIssue(
|
||||
ValidationSeverity severity,
|
||||
string code,
|
||||
string message,
|
||||
string? details = null)
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(severity, code, message, details));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all validation issues.
|
||||
/// </summary>
|
||||
/// <returns>This validator for method chaining.</returns>
|
||||
public QueryValidator Clear()
|
||||
{
|
||||
_issues.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets validation issues by severity level.
|
||||
/// </summary>
|
||||
/// <param name="severity">The severity to filter by.</param>
|
||||
/// <returns>Issues matching the severity level.</returns>
|
||||
public IReadOnlyList<QueryValidationIssue> GetIssuesBySeverity(ValidationSeverity severity)
|
||||
{
|
||||
return _issues.Where(i => i.Severity == severity).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a formatted validation report.
|
||||
/// </summary>
|
||||
/// <returns>A formatted string containing all validation issues.</returns>
|
||||
public string GetReport()
|
||||
{
|
||||
if (_issues.Count == 0)
|
||||
{
|
||||
return "✓ No validation issues found.";
|
||||
}
|
||||
|
||||
var report = new System.Text.StringBuilder();
|
||||
report.AppendLine($"Validation Report ({_issues.Count} issue{(_issues.Count != 1 ? "s" : "")}:");
|
||||
report.AppendLine();
|
||||
|
||||
var errors = GetIssuesBySeverity(ValidationSeverity.Error);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
report.AppendLine("ERRORS:");
|
||||
foreach (var issue in errors)
|
||||
{
|
||||
report.AppendLine($" • {issue}");
|
||||
}
|
||||
report.AppendLine();
|
||||
}
|
||||
|
||||
var warnings = GetIssuesBySeverity(ValidationSeverity.Warning);
|
||||
if (warnings.Count > 0)
|
||||
{
|
||||
report.AppendLine("WARNINGS:");
|
||||
foreach (var issue in warnings)
|
||||
{
|
||||
report.AppendLine($" • {issue}");
|
||||
}
|
||||
report.AppendLine();
|
||||
}
|
||||
|
||||
var infos = GetIssuesBySeverity(ValidationSeverity.Info);
|
||||
if (infos.Count > 0)
|
||||
{
|
||||
report.AppendLine("INFO:");
|
||||
foreach (var issue in infos)
|
||||
{
|
||||
report.AppendLine($" • {issue}");
|
||||
}
|
||||
}
|
||||
|
||||
return report.ToString();
|
||||
}
|
||||
|
||||
private void ValidateSelectClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown.SelectClause == null || string.IsNullOrWhiteSpace(breakdown.SelectClause.Clause))
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Error,
|
||||
"SELECT_MISSING",
|
||||
"SELECT clause is missing or empty",
|
||||
"Every query must specify which columns to select."));
|
||||
return;
|
||||
}
|
||||
|
||||
var selectClause = breakdown.SelectClause.Clause;
|
||||
|
||||
// Check for SELECT *
|
||||
if (selectClause.Trim() == "*")
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Warning,
|
||||
"SELECT_ALL_COLUMNS",
|
||||
"Query selects all columns with SELECT *",
|
||||
"Consider being explicit about which columns you need to avoid returning unnecessary data."));
|
||||
}
|
||||
|
||||
// Check for excessive columns
|
||||
var columnCount = selectClause.Split(',').Length;
|
||||
if (columnCount > 20)
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Warning,
|
||||
"SELECT_TOO_MANY",
|
||||
$"Query selects {columnCount} columns",
|
||||
"Consider narrowing the selection to reduce data transfer and improve performance."));
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateFromClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
if (breakdown.FromClause == null || string.IsNullOrWhiteSpace(breakdown.FromClause.Clause))
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Error,
|
||||
"FROM_MISSING",
|
||||
"FROM clause is missing",
|
||||
"Every query must specify a source table."));
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateWhereClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// No validation needed - WHERE is optional
|
||||
}
|
||||
|
||||
private void ValidateGroupByClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
var hasGroupBy = !string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause);
|
||||
var hasHaving = !string.IsNullOrWhiteSpace(breakdown.HavingClause?.Clause);
|
||||
|
||||
if (hasHaving && !hasGroupBy)
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Error,
|
||||
"HAVING_WITHOUT_GROUPBY",
|
||||
"HAVING clause found without GROUP BY",
|
||||
"HAVING must be used with GROUP BY to filter aggregated results."));
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateHavingClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// Validation delegated to ValidateGroupByClause
|
||||
}
|
||||
|
||||
private void ValidateOrderByClause(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// No validation needed - ORDER BY is optional
|
||||
}
|
||||
|
||||
private void ValidateCommonAntiPatterns(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
// Check for DELETE/UPDATE without WHERE (dangerous!)
|
||||
// Note: This is primarily for LINQ operations, but we can flag it for awareness
|
||||
if (string.IsNullOrWhiteSpace(breakdown.WhereClause?.Clause))
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Warning,
|
||||
"NO_WHERE_CLAUSE",
|
||||
"Query has no WHERE clause",
|
||||
"Consider whether this is intentional. Queries without WHERE clauses affect all rows."));
|
||||
}
|
||||
|
||||
// Check for missing ORDER BY on large results
|
||||
var hasOrderBy = !string.IsNullOrWhiteSpace(breakdown.OrderByClause?.Clause);
|
||||
var hasGroupBy = !string.IsNullOrWhiteSpace(breakdown.GroupByClause?.Clause);
|
||||
|
||||
if (!hasOrderBy && !hasGroupBy)
|
||||
{
|
||||
_issues.Add(new QueryValidationIssue(
|
||||
ValidationSeverity.Info,
|
||||
"NO_ORDER_BY",
|
||||
"Query has no ORDER BY clause",
|
||||
"Consider adding ORDER BY to ensure consistent result ordering, especially for pagination scenarios."));
|
||||
}
|
||||
|
||||
// Check for SELECT without FROM (invalid in most SQL dialects except for SELECT constants)
|
||||
var selectClause = breakdown.SelectClause?.Clause ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(selectClause) &&
|
||||
string.IsNullOrWhiteSpace(breakdown.FromClause?.Clause))
|
||||
{
|
||||
// This is already caught by ValidateFromClause
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of QueryValidator.
|
||||
/// </summary>
|
||||
/// <returns>A new QueryValidator instance.</returns>
|
||||
public static QueryValidator Create()
|
||||
{
|
||||
return new QueryValidator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a breakdown and returns a new validator with the results.
|
||||
/// </summary>
|
||||
/// <param name="breakdown">The breakdown to validate.</param>
|
||||
/// <returns>A new validator containing the validation results.</returns>
|
||||
public static QueryValidator ValidateQuery(LinqQueryBreakdown breakdown)
|
||||
{
|
||||
return new QueryValidator().Validate(breakdown);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace Strata.SqlTools.Visitors.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Expression visitor for analyzing LINQ to SQL expression trees.
|
||||
/// Extracts query components such as SELECT, WHERE, JOIN, GROUP BY, and ORDER BY.
|
||||
/// </summary>
|
||||
public class LinqExpressionVisitor : ExpressionVisitor
|
||||
{
|
||||
private readonly StringBuilder _whereBuilder = new();
|
||||
private readonly StringBuilder _orderByBuilder = new();
|
||||
private readonly List<string> _methodCalls = new();
|
||||
private bool _isInWhereClause;
|
||||
#pragma warning disable IDE0052, S4487
|
||||
private bool _isInSelectClause;
|
||||
private bool _isInOrderByClause;
|
||||
private bool _isInGroupByClause;
|
||||
private string? _tableName;
|
||||
#pragma warning restore IDE0052, S4487
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SELECT clause extracted from the expression.
|
||||
/// </summary>
|
||||
public string? SelectClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FROM clause (table name) extracted from the expression.
|
||||
/// </summary>
|
||||
public string? FromClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the WHERE clause extracted from the expression.
|
||||
/// </summary>
|
||||
public string? WhereClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ORDER BY clause extracted from the expression.
|
||||
/// </summary>
|
||||
public string? OrderByClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GROUP BY clause extracted from the expression.
|
||||
/// </summary>
|
||||
public string? GroupByClause { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of LINQ method calls in the query chain.
|
||||
/// </summary>
|
||||
public List<string> MethodCallChain => _methodCalls;
|
||||
|
||||
/// <summary>
|
||||
/// Visits a method call expression.
|
||||
/// </summary>
|
||||
protected override Expression VisitMethodCall(MethodCallExpression node)
|
||||
{
|
||||
var methodName = node.Method.Name;
|
||||
_methodCalls.Add(methodName);
|
||||
|
||||
switch (methodName)
|
||||
{
|
||||
case "Where":
|
||||
VisitWhereMethod(node);
|
||||
break;
|
||||
case "Select":
|
||||
VisitSelectMethod(node);
|
||||
break;
|
||||
case "OrderBy":
|
||||
case "OrderByDescending":
|
||||
case "ThenBy":
|
||||
case "ThenByDescending":
|
||||
VisitOrderByMethod(node);
|
||||
break;
|
||||
case "GroupBy":
|
||||
VisitGroupByMethod(node);
|
||||
break;
|
||||
case "Join":
|
||||
case "GroupJoin":
|
||||
VisitJoinMethod(node);
|
||||
break;
|
||||
case "Take":
|
||||
case "Skip":
|
||||
VisitTakeSkipMethod(node);
|
||||
break;
|
||||
default:
|
||||
// Visit the source expression
|
||||
Visit(node.Arguments[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a constant expression to extract the table name.
|
||||
/// </summary>
|
||||
protected override Expression VisitConstant(ConstantExpression node)
|
||||
{
|
||||
// Handle WHERE clause constants
|
||||
if (_isInWhereClause)
|
||||
{
|
||||
if (node.Value is string)
|
||||
{
|
||||
_whereBuilder.Append($"'{node.Value}'");
|
||||
}
|
||||
else if (node.Value != null)
|
||||
{
|
||||
_whereBuilder.Append(node.Value.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_whereBuilder.Append("NULL");
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Handle table name extraction
|
||||
if (node.Type.IsGenericType)
|
||||
{
|
||||
var genericType = node.Type.GetGenericTypeDefinition();
|
||||
if (genericType.Name.Contains("Table") || genericType.Name.Contains("Query"))
|
||||
{
|
||||
var entityType = node.Type.GetGenericArguments().FirstOrDefault();
|
||||
if (entityType != null)
|
||||
{
|
||||
_tableName = entityType.Name;
|
||||
FromClause = _tableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base.VisitConstant(node);
|
||||
}
|
||||
|
||||
private void VisitWhereMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the predicate
|
||||
if (node.Arguments.Count > 1)
|
||||
{
|
||||
var lambda = StripQuotes(node.Arguments[1]) as LambdaExpression;
|
||||
if (lambda != null)
|
||||
{
|
||||
_isInWhereClause = true;
|
||||
Visit(lambda.Body);
|
||||
_isInWhereClause = false;
|
||||
|
||||
if (_whereBuilder.Length > 0)
|
||||
{
|
||||
WhereClause = _whereBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitSelectMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the selector
|
||||
if (node.Arguments.Count > 1)
|
||||
{
|
||||
var lambda = StripQuotes(node.Arguments[1]) as LambdaExpression;
|
||||
if (lambda != null)
|
||||
{
|
||||
_isInSelectClause = true;
|
||||
var selectExpression = ExtractSelectExpression(lambda.Body);
|
||||
_isInSelectClause = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(selectExpression))
|
||||
{
|
||||
SelectClause = selectExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitOrderByMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the key selector
|
||||
if (node.Arguments.Count > 1)
|
||||
{
|
||||
var lambda = StripQuotes(node.Arguments[1]) as LambdaExpression;
|
||||
if (lambda != null)
|
||||
{
|
||||
_isInOrderByClause = true;
|
||||
var orderByExpression = ExtractMemberName(lambda.Body);
|
||||
_isInOrderByClause = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(orderByExpression))
|
||||
{
|
||||
var direction = node.Method.Name.Contains("Descending") ? " DESC" : " ASC";
|
||||
|
||||
if (_orderByBuilder.Length > 0)
|
||||
{
|
||||
_orderByBuilder.Append(", ");
|
||||
}
|
||||
_orderByBuilder.Append(orderByExpression + direction);
|
||||
OrderByClause = _orderByBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitGroupByMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the key selector
|
||||
if (node.Arguments.Count > 1)
|
||||
{
|
||||
var lambda = StripQuotes(node.Arguments[1]) as LambdaExpression;
|
||||
if (lambda != null)
|
||||
{
|
||||
_isInGroupByClause = true;
|
||||
var groupByExpression = ExtractMemberName(lambda.Body);
|
||||
_isInGroupByClause = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(groupByExpression))
|
||||
{
|
||||
GroupByClause = groupByExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitJoinMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// For joins, we'd need more complex logic to extract full join information
|
||||
// This is a simplified version
|
||||
_methodCalls.Add($"{node.Method.Name} (complex join analysis not fully implemented)");
|
||||
}
|
||||
|
||||
private void VisitTakeSkipMethod(MethodCallExpression node)
|
||||
{
|
||||
// Visit the source
|
||||
Visit(node.Arguments[0]);
|
||||
|
||||
// Extract the count
|
||||
if (node.Arguments.Count > 1 && node.Arguments[1] is ConstantExpression constant)
|
||||
{
|
||||
_methodCalls.Add($"{node.Method.Name}({constant.Value})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a binary expression (e.g., comparisons, logical operations).
|
||||
/// </summary>
|
||||
protected override Expression VisitBinary(BinaryExpression node)
|
||||
{
|
||||
if (_isInWhereClause)
|
||||
{
|
||||
_whereBuilder.Append("(");
|
||||
Visit(node.Left);
|
||||
|
||||
_whereBuilder.Append($" {GetOperator(node.NodeType)} ");
|
||||
|
||||
Visit(node.Right);
|
||||
_whereBuilder.Append(")");
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
return base.VisitBinary(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a member access expression.
|
||||
/// </summary>
|
||||
protected override Expression VisitMember(MemberExpression node)
|
||||
{
|
||||
if (_isInWhereClause)
|
||||
{
|
||||
var memberName = GetFullMemberName(node);
|
||||
_whereBuilder.Append(memberName);
|
||||
return node;
|
||||
}
|
||||
|
||||
return base.VisitMember(node);
|
||||
}
|
||||
|
||||
private string ExtractSelectExpression(Expression expression)
|
||||
{
|
||||
if (expression is NewExpression newExpr)
|
||||
{
|
||||
var members = new List<string>();
|
||||
for (int i = 0; i < newExpr.Arguments.Count; i++)
|
||||
{
|
||||
var memberName = ExtractMemberName(newExpr.Arguments[i]);
|
||||
var alias = newExpr.Members?[i].Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(alias) && alias != memberName)
|
||||
{
|
||||
members.Add($"{memberName} AS {alias}");
|
||||
}
|
||||
else
|
||||
{
|
||||
members.Add(memberName);
|
||||
}
|
||||
}
|
||||
return string.Join(", ", members);
|
||||
}
|
||||
|
||||
var name = ExtractMemberName(expression);
|
||||
return string.IsNullOrEmpty(name) ? "*" : name;
|
||||
}
|
||||
|
||||
private string ExtractMemberName(Expression expression)
|
||||
{
|
||||
if (expression is MemberExpression member)
|
||||
{
|
||||
return GetFullMemberName(member);
|
||||
}
|
||||
|
||||
if (expression is ParameterExpression param)
|
||||
{
|
||||
return "*";
|
||||
}
|
||||
|
||||
if (expression is MethodCallExpression methodCall)
|
||||
{
|
||||
return $"{methodCall.Method.Name}(...)";
|
||||
}
|
||||
|
||||
return expression.ToString();
|
||||
}
|
||||
|
||||
private string GetFullMemberName(MemberExpression expression)
|
||||
{
|
||||
var parts = new Stack<string>();
|
||||
var current = expression;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
parts.Push(current.Member.Name);
|
||||
|
||||
if (current.Expression is MemberExpression memberExpr)
|
||||
{
|
||||
current = memberExpr;
|
||||
}
|
||||
else if (current.Expression is ParameterExpression paramExpr)
|
||||
{
|
||||
// Use parameter name as table alias if it's not the default
|
||||
if (paramExpr.Name != null && paramExpr.Name.Length == 1)
|
||||
{
|
||||
parts.Push(paramExpr.Name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(".", parts);
|
||||
}
|
||||
|
||||
private string GetOperator(ExpressionType nodeType)
|
||||
{
|
||||
return nodeType switch
|
||||
{
|
||||
ExpressionType.Equal => "=",
|
||||
ExpressionType.NotEqual => "!=",
|
||||
ExpressionType.GreaterThan => ">",
|
||||
ExpressionType.GreaterThanOrEqual => ">=",
|
||||
ExpressionType.LessThan => "<",
|
||||
ExpressionType.LessThanOrEqual => "<=",
|
||||
ExpressionType.AndAlso => "AND",
|
||||
ExpressionType.OrElse => "OR",
|
||||
ExpressionType.Add => "+",
|
||||
ExpressionType.Subtract => "-",
|
||||
ExpressionType.Multiply => "*",
|
||||
ExpressionType.Divide => "/",
|
||||
_ => nodeType.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private static Expression StripQuotes(Expression expression)
|
||||
{
|
||||
while (expression.NodeType == ExpressionType.Quote)
|
||||
{
|
||||
expression = ((UnaryExpression)expression).Operand;
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Arithmetic;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Generates markdown documentation for Expression trees.
|
||||
/// Creates human-readable documentation with expression structure, type information, and visual representations.
|
||||
/// </summary>
|
||||
public class ExpressionGenerator : IVisitor<string>
|
||||
{
|
||||
private int _indentLevel = 0;
|
||||
private readonly string _indentString = " ";
|
||||
|
||||
/// <summary>
|
||||
/// Generates markdown documentation from an Expression tree.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to document.</param>
|
||||
/// <param name="title">Optional title for the documentation.</param>
|
||||
/// <returns>A markdown formatted string documenting the expression.</returns>
|
||||
public string GenerateMarkdown(Expression expression, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("## Expression Structure");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("```");
|
||||
_indentLevel = 0;
|
||||
sb.AppendLine(expression.Accept(this));
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Expression Type");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"**Type:** `{expression.GetType().Name}`");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Mermaid Diagram");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(GenerateMermaidDiagram(expression));
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("## Mathematical Expression");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(GenerateMathematicalExpression(expression));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mathematical expression using LaTeX notation for GitHub markdown.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to convert to mathematical notation.</param>
|
||||
/// <param name="inline">If true, generates inline math ($...$), otherwise block math ($$...$$).</param>
|
||||
/// <returns>A string containing the LaTeX mathematical expression.</returns>
|
||||
public static string GenerateMathematicalExpression(Expression expression, bool inline = false)
|
||||
{
|
||||
var latex = ConvertToLatex(expression);
|
||||
return inline ? $"${latex}$" : $"$$\n{latex}\n$$";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mathematical expression from raw LaTeX in markdown format.
|
||||
/// </summary>
|
||||
/// <param name="latex">The LaTeX expression.</param>
|
||||
/// <param name="format">The format to use: "dollar" for $/$$ delimiters, "math" for ```math code fence.</param>
|
||||
/// <param name="inline">If true and format is "dollar", generates inline math ($...$), otherwise block math ($$...$$). Ignored for "math" format.</param>
|
||||
/// <returns>A string containing the formatted mathematical expression.</returns>
|
||||
public static string GenerateRawMathematicalExpression(string latex, string format = "dollar", bool inline = false)
|
||||
{
|
||||
return format.ToLower() switch
|
||||
{
|
||||
"math" => $"```math\n{latex}\n```",
|
||||
_ => inline ? $"${latex}$" : $"$$\n{latex}\n$$"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ConvertToLatex(Expression expression)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
// Arithmetic expressions
|
||||
ArithmeticExpression arith => ConvertArithmeticToLatex(arith),
|
||||
|
||||
// Comparison expressions
|
||||
ComparisonOperatorExpression comp => ConvertComparisonToLatex(comp),
|
||||
|
||||
// Logical expressions
|
||||
AndExpression and => $"({ConvertToLatex(and.ExpressionA)} \\land {ConvertToLatex(and.ExpressionB)})",
|
||||
OrExpression or => $"({ConvertToLatex(or.ExpressionA)} \\lor {ConvertToLatex(or.ExpressionB)})",
|
||||
NotExpression not => $"\\neg({ConvertToLatex(not.ExpressionA)})",
|
||||
|
||||
// Literals
|
||||
NumberLiteralExpression num => num.Value.ToString() ?? "0",
|
||||
StringLiteralExpression str => $"\\text{{\"{EscapeLatex(str.Value)}\"}}",
|
||||
BooleanLiteralExpression b => b.Value ? "\\text{true}" : "\\text{false}",
|
||||
NullLiteralExpression => "\\text{NULL}",
|
||||
|
||||
// Column expressions
|
||||
ColumnExpression col => $"\\text{{{EscapeLatex(col.ColumnName)}}}",
|
||||
|
||||
// Parameter expressions
|
||||
ParameterExpression param => $"@{EscapeLatex(param.ParameterName)}",
|
||||
|
||||
// Case expressions (before FunctionExpression since it's a subclass)
|
||||
CaseExpression caseExpr => ConvertCaseToLatex(caseExpr),
|
||||
|
||||
// Functions
|
||||
FunctionExpression func => ConvertFunctionToLatex(func),
|
||||
|
||||
// Between expressions
|
||||
BetweenExpression between => $"{ConvertToLatex(between.Expression)} \\in [{ConvertToLatex(between.LowerBound)}, {ConvertToLatex(between.UpperBound)}]",
|
||||
|
||||
// IN expressions
|
||||
InExpression inExpr => $"{ConvertToLatex(inExpr.SearchExpression)} \\in \\{{{string.Join(", ", inExpr.ValuesToCompare.Select(ConvertToLatex))}\\}}",
|
||||
|
||||
// LIKE expressions
|
||||
LikeExpression like => $"{ConvertToLatex(like.Subject)} \\approx \\text{{\"{EscapeLatex(ConvertExpressionToString(like.Pattern))}\"}}",
|
||||
_ => $"\\text{{{EscapeLatex(expression.GetType().Name)}}}"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ConvertArithmeticToLatex(ArithmeticExpression arith)
|
||||
{
|
||||
var left = ConvertToLatex(arith.ExpressionA);
|
||||
var right = ConvertToLatex(arith.ExpressionB);
|
||||
|
||||
var op = arith.ArithmeticOperator switch
|
||||
{
|
||||
"+" => "+",
|
||||
"-" => "-",
|
||||
"*" => "\\times",
|
||||
"/" => "\\div",
|
||||
"%" => "\\bmod",
|
||||
_ => "?"
|
||||
};
|
||||
|
||||
return $"({left} {op} {right})";
|
||||
}
|
||||
|
||||
private static string ConvertComparisonToLatex(ComparisonOperatorExpression comp)
|
||||
{
|
||||
var left = ConvertToLatex(comp.ExpressionA);
|
||||
var right = ConvertToLatex(comp.ExpressionB);
|
||||
|
||||
var op = comp.Operator switch
|
||||
{
|
||||
"=" => "=",
|
||||
"<>" => "\\neq",
|
||||
"!=" => "\\neq",
|
||||
">" => ">",
|
||||
">=" => "\\geq",
|
||||
"<" => "<",
|
||||
"<=" => "\\leq",
|
||||
_ => "?"
|
||||
};
|
||||
|
||||
return $"({left} {op} {right})";
|
||||
}
|
||||
|
||||
private static string ConvertFunctionToLatex(FunctionExpression func)
|
||||
{
|
||||
var args = string.Join(", ", func.Arguments.Select(ConvertToLatex));
|
||||
var funcName = EscapeLatex(func.FunctionName);
|
||||
|
||||
return func.FunctionName.ToUpper() switch
|
||||
{
|
||||
// Aggregate functions
|
||||
"COUNT" => $"\\text{{COUNT}}({args})",
|
||||
"SUM" => $"\\sum({args})",
|
||||
"AVG" => $"\\text{{AVG}}({args})",
|
||||
"MIN" => $"\\min({args})",
|
||||
"MAX" => $"\\max({args})",
|
||||
|
||||
// Math functions
|
||||
"ABS" => $"|{args}|",
|
||||
"SQRT" => $"\\sqrt{{{args}}}",
|
||||
"POWER" when func.Arguments.Length >= 2 =>
|
||||
$"{ConvertToLatex(func.Arguments[0])}^{{{ConvertToLatex(func.Arguments[1])}}}",
|
||||
"LOG" => $"\\log({args})",
|
||||
"EXP" => $"e^{{{args}}}",
|
||||
|
||||
// Default
|
||||
_ => $"\\text{{{funcName}}}({args})"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ConvertCaseToLatex(CaseExpression caseExpr)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("\\begin{cases}\n");
|
||||
|
||||
foreach (var (condition, result) in caseExpr.ConditionResultPairs)
|
||||
{
|
||||
sb.Append($" {ConvertToLatex(result)} & \\text{{if }} {ConvertToLatex(condition)} \\\\\n");
|
||||
}
|
||||
|
||||
if (caseExpr.ElseResultExpression is not null)
|
||||
{
|
||||
sb.Append($" {ConvertToLatex(caseExpr.ElseResultExpression)} & \\text{{otherwise}}\n");
|
||||
}
|
||||
|
||||
sb.Append("\\end{cases}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ConvertExpressionToString(Expression expression)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
StringLiteralExpression str => str.Value,
|
||||
_ => expression.ToString() ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
private static string EscapeLatex(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("{", "\\{")
|
||||
.Replace("}", "\\}")
|
||||
.Replace("_", "\\_")
|
||||
.Replace("$", "\\$")
|
||||
.Replace("%", "\\%")
|
||||
.Replace("&", "\\&")
|
||||
.Replace("#", "\\#");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid tree diagram from an Expression tree.
|
||||
/// </summary>
|
||||
private string GenerateMermaidDiagram(Expression expression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("graph TD");
|
||||
sb.AppendLine();
|
||||
|
||||
int nodeCounter = 0;
|
||||
var nodeMap = new Dictionary<object, int>();
|
||||
GenerateMermaidNodes(expression, sb, nodeMap, ref nodeCounter);
|
||||
|
||||
sb.AppendLine("```");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private int GenerateMermaidNodes(Expression expression, StringBuilder sb, Dictionary<object, int> nodeMap, ref int nodeCounter)
|
||||
{
|
||||
var currentNode = nodeCounter++;
|
||||
nodeMap[expression] = currentNode;
|
||||
|
||||
var nodeLabel = GetNodeLabel(expression);
|
||||
var nodeShape = GetNodeShape(expression);
|
||||
|
||||
sb.AppendLine($" Node{currentNode}{nodeShape[0]}\"{EscapeMarkdown(nodeLabel)}\"{nodeShape[1]}");
|
||||
|
||||
// Process child expressions
|
||||
switch (expression)
|
||||
{
|
||||
case ComparisonOperatorExpression comp:
|
||||
var leftId = GenerateMermaidNodes(comp.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
var rightId = GenerateMermaidNodes(comp.ExpressionB, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} --> Node{leftId}");
|
||||
sb.AppendLine($" Node{currentNode} --> Node{rightId}");
|
||||
break;
|
||||
|
||||
case AndExpression and:
|
||||
var andLeftId = GenerateMermaidNodes(and.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
var andRightId = GenerateMermaidNodes(and.ExpressionB, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} -->|Left| Node{andLeftId}");
|
||||
sb.AppendLine($" Node{currentNode} -->|Right| Node{andRightId}");
|
||||
break;
|
||||
|
||||
case OrExpression or:
|
||||
var orLeftId = GenerateMermaidNodes(or.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
var orRightId = GenerateMermaidNodes(or.ExpressionB, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} -->|Left| Node{orLeftId}");
|
||||
sb.AppendLine($" Node{currentNode} -->|Right| Node{orRightId}");
|
||||
break;
|
||||
|
||||
case NotExpression not:
|
||||
var notId = GenerateMermaidNodes(not.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} --> Node{notId}");
|
||||
break;
|
||||
|
||||
case ArithmeticExpression arith:
|
||||
var arithmLeftId = GenerateMermaidNodes(arith.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
var arithmRightId = GenerateMermaidNodes(arith.ExpressionB, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} --> Node{arithmLeftId}");
|
||||
sb.AppendLine($" Node{currentNode} --> Node{arithmRightId}");
|
||||
break;
|
||||
|
||||
case FunctionExpression func:
|
||||
foreach (var arg in func.Arguments)
|
||||
{
|
||||
var argId = GenerateMermaidNodes(arg, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} --> Node{argId}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
private static string GetNodeLabel(Expression expression)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
NumberLiteralExpression num => $"Number: {num.Value}",
|
||||
StringLiteralExpression str => $"String: {TruncateText(str.Value, 20)}",
|
||||
DateTimeLiteralExpression dt => $"DateTime: {dt.Value:yyyy-MM-dd}",
|
||||
BooleanLiteralExpression b => $"Boolean: {b.Value}",
|
||||
NullLiteralExpression => "NULL",
|
||||
ComparisonOperatorExpression comp => $"Comparison: {comp.Operator}",
|
||||
AndExpression => "AND",
|
||||
OrExpression => "OR",
|
||||
NotExpression => "NOT",
|
||||
ArithmeticExpression arith => $"Arithmetic: {arith.ArithmeticOperator}",
|
||||
FunctionExpression func => $"Function: {func.FunctionName}",
|
||||
ParameterExpression param => $"Parameter: @{param.ParameterName}",
|
||||
_ => expression.GetType().Name
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] GetNodeShape(Expression expression)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
NumberLiteralExpression or StringLiteralExpression or DateTimeLiteralExpression or BooleanLiteralExpression or NullLiteralExpression => new[] { "[", "]" },
|
||||
ComparisonOperatorExpression => new[] { "{", "}" },
|
||||
AndExpression or OrExpression or NotExpression => new[] { "{", "}" },
|
||||
FunctionExpression => new[] { "[[", "]]" },
|
||||
_ => new[] { "(", ")" }
|
||||
};
|
||||
}
|
||||
|
||||
private string Indent() => new string(' ', _indentLevel * _indentString.Length);
|
||||
|
||||
private static string EscapeMarkdown(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\"", """)
|
||||
.Replace("[", "[")
|
||||
.Replace("]", "]");
|
||||
}
|
||||
|
||||
private static string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
#region IVisitor Implementation
|
||||
|
||||
public string VisitTableSource(TableSource tableSource)
|
||||
{
|
||||
return $"{Indent()}TableSource: {tableSource.TableName}";
|
||||
}
|
||||
|
||||
public string VisitColumnExpression<TSource>(ColumnExpression<TSource> column) where TSource : SelectSource
|
||||
{
|
||||
return $"{Indent()}Column: {column.ColumnName}";
|
||||
}
|
||||
|
||||
public string VisitSelectClauseColumn(SelectClauseColumn selectClauseColumn)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}SelectClauseColumn:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(selectClauseColumn.Expression.Accept(this));
|
||||
if (!string.IsNullOrWhiteSpace(selectClauseColumn.Alias))
|
||||
{
|
||||
sb.AppendLine($"{Indent()}Alias: {selectClauseColumn.Alias}");
|
||||
}
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitParameterExpression(ParameterExpression parameterExpression)
|
||||
{
|
||||
return $"{Indent()}Parameter: @{parameterExpression.ParameterName}";
|
||||
}
|
||||
|
||||
public string VisitNumberLiteralExpression(NumberLiteralExpression numberLiteral)
|
||||
{
|
||||
return $"{Indent()}Number: {numberLiteral.Value}";
|
||||
}
|
||||
|
||||
public string VisitStringLiteralExpression(StringLiteralExpression stringLiteral)
|
||||
{
|
||||
return $"{Indent()}String: '{stringLiteral.Value}'";
|
||||
}
|
||||
|
||||
public string VisitDateTimeLiteralExpression(DateTimeLiteralExpression dateTimeLiteral)
|
||||
{
|
||||
return $"{Indent()}DateTime: {dateTimeLiteral.Value:yyyy-MM-dd HH:mm:ss}";
|
||||
}
|
||||
|
||||
public string VisitNullLiteralExpression(NullLiteralExpression nullLiteral)
|
||||
{
|
||||
return $"{Indent()}NULL";
|
||||
}
|
||||
|
||||
public string VisitBooleanLiteralExpression(BooleanLiteralExpression booleanLiteral)
|
||||
{
|
||||
return $"{Indent()}Boolean: {booleanLiteral.Value}";
|
||||
}
|
||||
|
||||
public string VisitParameterLiteralExpression(ParameterLiteralExpression parameterLiteral)
|
||||
{
|
||||
return $"{Indent()}Parameter: {parameterLiteral.Value}";
|
||||
}
|
||||
|
||||
public string VisitSymbolLiteralExpression(SymbolLiteralExpression symbolLiteral)
|
||||
{
|
||||
return $"{Indent()}Symbol: {symbolLiteral.Value}";
|
||||
}
|
||||
|
||||
public string VisitComparisonExpression(ComparisonOperatorExpression comparison)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Comparison ({comparison.Operator}):");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Left:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(comparison.ExpressionA.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Right:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(comparison.ExpressionB.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitAndExpression(AndExpression logical)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}AND:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(logical.ExpressionA.Accept(this));
|
||||
sb.AppendLine(logical.ExpressionB.Accept(this));
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitOrExpression(OrExpression logical)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}OR:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(logical.ExpressionA.Accept(this));
|
||||
sb.AppendLine(logical.ExpressionB.Accept(this));
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitNotExpression(NotExpression logical)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}NOT:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(logical.ExpressionA.Accept(this));
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitInExpression(InExpression inExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}IN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Search Expression:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(inExpression.SearchExpression.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Values:");
|
||||
_indentLevel++;
|
||||
foreach (var value in inExpression.ValuesToCompare)
|
||||
{
|
||||
sb.AppendLine(value.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitNotInExpression(NotInExpression inExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}NOT IN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Search Expression:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(inExpression.SearchExpression.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Values:");
|
||||
_indentLevel++;
|
||||
foreach (var value in inExpression.ValuesToCompare)
|
||||
{
|
||||
sb.AppendLine(value.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitLikeExpression(LikeExpression likeExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}LIKE (Case {(likeExpression.CaseInsensitive ? "Insensitive" : "Sensitive")}):");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Subject:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(likeExpression.Subject.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Pattern:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(likeExpression.Pattern.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitNotLikeExpression(NotLikeExpression notLikeExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}NOT LIKE:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Subject:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(notLikeExpression.Subject.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Pattern:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(notLikeExpression.Pattern.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitBetweenExpression(BetweenExpression betweenExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}BETWEEN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Expression:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(betweenExpression.Expression.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Lower Bound:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(betweenExpression.LowerBound.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Upper Bound:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(betweenExpression.UpperBound.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitFunctionExpression(FunctionExpression function)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Function: {function.FunctionName}");
|
||||
if (function.Arguments.Any())
|
||||
{
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Arguments:");
|
||||
_indentLevel++;
|
||||
foreach (var arg in function.Arguments)
|
||||
{
|
||||
sb.AppendLine(arg.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitAggregateFunctionExpression(AggregateFunctionExpression aggregateFunction)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Aggregate Function: {aggregateFunction.FunctionName}");
|
||||
if (aggregateFunction.Arguments.Any())
|
||||
{
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Arguments:");
|
||||
_indentLevel++;
|
||||
foreach (var arg in aggregateFunction.Arguments)
|
||||
{
|
||||
sb.AppendLine(arg.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitCaseFunctionExpression(CaseExpression caseFunction)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}CASE:");
|
||||
_indentLevel++;
|
||||
foreach (var (condition, result) in caseFunction.ConditionResultPairs)
|
||||
{
|
||||
sb.AppendLine($"{Indent()}WHEN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(condition.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}THEN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(result.Accept(this));
|
||||
_indentLevel--;
|
||||
}
|
||||
if (caseFunction.ElseResultExpression is not null)
|
||||
{
|
||||
sb.AppendLine($"{Indent()}ELSE:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(caseFunction.ElseResultExpression.Accept(this));
|
||||
_indentLevel--;
|
||||
}
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitArithmeticExpression(ArithmeticExpression arithmeticExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Arithmetic ({arithmeticExpression.ArithmeticOperator}):");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Left:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(arithmeticExpression.ExpressionA.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Right:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(arithmeticExpression.ExpressionB.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitInputPropertyExpression(InputPropertyExpression inputPropertyExpression)
|
||||
{
|
||||
return $"{Indent()}InputProperty: {inputPropertyExpression.DataKeyLookup}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.Visitors.SqlServer;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Generates simplified markdown documentation for Expression trees focused on readability.
|
||||
/// </summary>
|
||||
public class SimpleExpressionGenerator
|
||||
{
|
||||
private readonly CommandVisitor _sqlVisitor = new();
|
||||
|
||||
/// <summary>
|
||||
/// Generates a simple markdown document from an Expression.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to document.</param>
|
||||
/// <param name="title">Optional title for the documentation.</param>
|
||||
/// <returns>A markdown formatted string documenting the expression.</returns>
|
||||
public string GenerateMarkdown(Expression expression, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("## Expression");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("```sql");
|
||||
sb.AppendLine(expression.Accept(_sqlVisitor));
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Type Information");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"- **Expression Type:** `{expression.GetType().Name}`");
|
||||
sb.AppendLine($"- **Namespace:** `{expression.GetType().Namespace}`");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Description");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(GetExpressionDescription(expression));
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a comparison table for multiple expressions.
|
||||
/// </summary>
|
||||
/// <param name="expressions">Dictionary of expression names to expressions.</param>
|
||||
/// <param name="title">Optional title for the table.</param>
|
||||
/// <returns>A markdown formatted comparison table.</returns>
|
||||
public string GenerateComparisonTable(Dictionary<string, Expression> expressions, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("| Name | Expression | Type |");
|
||||
sb.AppendLine("|------|------------|------|");
|
||||
|
||||
foreach (var (name, expr) in expressions)
|
||||
{
|
||||
var sql = expr.Accept(_sqlVisitor).Replace("|", "\\|").Replace("\n", " ");
|
||||
var type = expr.GetType().Name;
|
||||
sb.AppendLine($"| {name} | `{sql}` | `{type}` |");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a bulleted list of expressions.
|
||||
/// </summary>
|
||||
/// <param name="expressions">List of expressions to document.</param>
|
||||
/// <param name="title">Optional title for the list.</param>
|
||||
/// <returns>A markdown formatted bulleted list.</returns>
|
||||
public string GenerateBulletList(IEnumerable<Expression> expressions, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"## {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var expr in expressions)
|
||||
{
|
||||
var sql = expr.Accept(_sqlVisitor).Replace("\n", " ");
|
||||
sb.AppendLine($"- `{sql}` - *{expr.GetType().Name}*");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GetExpressionDescription(Expression expression)
|
||||
{
|
||||
var typeName = expression.GetType().Name;
|
||||
|
||||
return typeName switch
|
||||
{
|
||||
"AndExpression" => "A logical AND expression that combines two boolean expressions. Both expressions must evaluate to true for the result to be true.",
|
||||
"OrExpression" => "A logical OR expression that combines two boolean expressions. Either expression can evaluate to true for the result to be true.",
|
||||
"NotExpression" => "A logical NOT expression that negates a boolean expression.",
|
||||
"ComparisonOperatorExpression" => "A comparison expression that compares two values using an operator (=, <>, <, >, <=, >=).",
|
||||
"ArithmeticExpression" => "An arithmetic expression that performs mathematical operations (+, -, *, /) on numeric values.",
|
||||
"FunctionExpression" => "A SQL function call expression that invokes a database function with arguments.",
|
||||
"AggregateFunctionExpression" => "An aggregate function expression (SUM, COUNT, AVG, MIN, MAX) that operates on sets of values.",
|
||||
"CaseExpression" => "A CASE expression that provides conditional logic similar to if-then-else statements.",
|
||||
"InExpression" => "An IN expression that checks if a value exists in a set of values.",
|
||||
"BetweenExpression" => "A BETWEEN expression that checks if a value falls within a range.",
|
||||
"LikeExpression" => "A LIKE expression that performs pattern matching on strings using wildcards.",
|
||||
"NumberLiteralExpression" => "A numeric literal value.",
|
||||
"StringLiteralExpression" => "A string literal value enclosed in quotes.",
|
||||
"DateTimeLiteralExpression" => "A date/time literal value.",
|
||||
"BooleanLiteralExpression" => "A boolean literal value (true/false).",
|
||||
"NullLiteralExpression" => "A NULL literal value representing absence of data.",
|
||||
"ParameterExpression" => "A parameterized value placeholder that will be substituted at runtime.",
|
||||
"ColumnExpression" => "A reference to a database column from a table or view.",
|
||||
_ => $"A {typeName} expression."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagram markdown from LINQ to SQL QueryBreakdown objects.
|
||||
/// Creates flowchart visualizations showing the LINQ query structure and flow.
|
||||
/// </summary>
|
||||
public class QueryBreakdownGenerator
|
||||
{
|
||||
private readonly SqlServer.QueryBreakdownGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the QueryBreakdownGenerator class.
|
||||
/// </summary>
|
||||
public QueryBreakdownGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.QueryBreakdownGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from a LINQ to SQL QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(LinqQueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
// Since LinqQueryBreakdown inherits from SqlServer.QueryBreakdown,
|
||||
// we can use the base generator which works with the shared properties
|
||||
return _baseGenerator.GenerateMermaidDiagram(queryBreakdown, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram showing the LINQ method call chain.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid flowchart showing method calls.</returns>
|
||||
public string GenerateMethodChainDiagram(LinqQueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart LR");
|
||||
sb.AppendLine();
|
||||
|
||||
if (queryBreakdown.MethodCallChain.Count == 0)
|
||||
{
|
||||
sb.AppendLine(" Start([IQueryable]) --> End([Result])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" Start([IQueryable])");
|
||||
|
||||
for (int i = 0; i < queryBreakdown.MethodCallChain.Count; i++)
|
||||
{
|
||||
var method = queryBreakdown.MethodCallChain[i];
|
||||
var nodeId = $"M{i}";
|
||||
var prevNodeId = i == 0 ? "Start" : $"M{i - 1}";
|
||||
|
||||
sb.AppendLine($" {nodeId}[\"{method}\"]");
|
||||
sb.AppendLine($" {prevNodeId} --> {nodeId}");
|
||||
}
|
||||
|
||||
var lastNodeId = $"M{queryBreakdown.MethodCallChain.Count - 1}";
|
||||
sb.AppendLine($" {lastNodeId} --> End([Result])");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a combined diagram showing both the query structure and method chain.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing both diagrams.</returns>
|
||||
public string GenerateCombinedDiagram(LinqQueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"## {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Method chain
|
||||
sb.AppendLine("### LINQ Method Chain");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateMethodChainDiagram(queryBreakdown));
|
||||
|
||||
// SQL Structure
|
||||
sb.AppendLine("### SQL Query Structure");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateMermaidDiagram(queryBreakdown));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagrams for LINQ to SQL statements, including sequence diagrams
|
||||
/// for statement execution flow and entity-relationship diagrams.
|
||||
/// </summary>
|
||||
public class SqlStatementGenerator
|
||||
{
|
||||
private readonly SqlServer.SqlStatementGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SqlStatementGenerator class.
|
||||
/// </summary>
|
||||
public SqlStatementGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.SqlStatementGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid sequence diagram showing LINQ to SQL statement execution flow.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The LINQ to SQL breakdown object.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid sequence diagram markdown.</returns>
|
||||
public string GenerateSequenceDiagram(ISqlBreakdown sqlBreakdown, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateSequenceDiagram(sqlBreakdown, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid entity-relationship diagram from a SQL breakdown.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The SQL breakdown containing query information.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid ER diagram markdown.</returns>
|
||||
public string GenerateEntityRelationshipDiagram(ISqlBreakdown sqlBreakdown, string? title = null)
|
||||
{
|
||||
//Extract table names from breakdown - just use FROM clause for now
|
||||
var queryBreakdown = sqlBreakdown as IQueryBreakdown;
|
||||
var tableNames = new List<string>();
|
||||
|
||||
if (queryBreakdown != null && !string.IsNullOrWhiteSpace(queryBreakdown.FromClause?.ToString()))
|
||||
{
|
||||
tableNames.Add(queryBreakdown.FromClause.ToString());
|
||||
}
|
||||
|
||||
return _baseGenerator.GenerateEntityRelationshipDiagram(tableNames, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a diagram showing LINQ execution pipeline from a SQL breakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The query breakdown containing query information.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid diagram markdown.</returns>
|
||||
public string GenerateLinqPipelineDiagram(IQueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("sequenceDiagram");
|
||||
sb.AppendLine(" participant Client as Client Application");
|
||||
sb.AppendLine(" participant LINQ as LINQ Provider");
|
||||
sb.AppendLine(" participant ET as Expression Tree");
|
||||
sb.AppendLine(" participant SQL as SQL Generator");
|
||||
sb.AppendLine(" participant DB as Database");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" Client->>LINQ: LINQ Query");
|
||||
sb.AppendLine(" activate LINQ");
|
||||
|
||||
// Check if there's a WHERE clause
|
||||
var whereClause = queryBreakdown.WhereClause?.Clause;
|
||||
if (!string.IsNullOrWhiteSpace(whereClause))
|
||||
{
|
||||
sb.AppendLine(" LINQ->>ET: Where Predicate");
|
||||
sb.AppendLine(" activate ET");
|
||||
}
|
||||
|
||||
// Check if there's a custom SELECT
|
||||
var selectClause = queryBreakdown.SelectClause?.Clause;
|
||||
if (!string.IsNullOrWhiteSpace(selectClause) && selectClause.Trim() != "*")
|
||||
{
|
||||
sb.AppendLine(" LINQ->>ET: Select Projection");
|
||||
if (string.IsNullOrWhiteSpace(whereClause))
|
||||
{
|
||||
sb.AppendLine(" activate ET");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine(" ET->>SQL: Expression Tree");
|
||||
sb.AppendLine(" deactivate ET");
|
||||
sb.AppendLine(" SQL->>DB: Generate SQL");
|
||||
sb.AppendLine(" activate DB");
|
||||
sb.AppendLine(" DB-->>SQL: Result Set");
|
||||
sb.AppendLine(" deactivate DB");
|
||||
sb.AppendLine(" SQL-->>LINQ: Mapped Objects");
|
||||
sb.AppendLine(" LINQ-->>Client: IEnumerable Result");
|
||||
sb.AppendLine(" deactivate LINQ");
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.Breakdowns.PostgreSql;
|
||||
using QuerySummary = Strata.SqlTools.Breakdowns.SqlServer.QuerySummary;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Markdown documentation from QueryBreakdownCollection objects for PostgreSQL.
|
||||
/// Creates comprehensive reports including collection summaries, parameter analysis, and batch flow visualization
|
||||
/// with PostgreSQL-specific features.
|
||||
/// </summary>
|
||||
public static class QueryBreakdownCollectionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a comprehensive collection report in Markdown format with PostgreSQL-specific information.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to document.</param>
|
||||
/// <param name="title">Optional title for the report.</param>
|
||||
/// <returns>A string containing the Markdown documentation.</returns>
|
||||
public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Collection Summary
|
||||
sb.Append(GenerateCollectionSummary(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Parameter Analysis
|
||||
sb.Append(GenerateParameterAnalysis(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Query Composition Report
|
||||
sb.Append(GenerateQueryCompositionReport(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary section for the collection.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to summarize.</param>
|
||||
/// <returns>Markdown summary section.</returns>
|
||||
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Collection Summary");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("| Metric | Value |");
|
||||
sb.AppendLine("|--------|-------|");
|
||||
sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |");
|
||||
sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |");
|
||||
sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |");
|
||||
sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a parameter analysis report with PostgreSQL parameter syntax support.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
|
||||
/// <returns>Markdown parameter analysis section.</returns>
|
||||
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var paramReport = collection.GetParameterUsageReport().ToList();
|
||||
|
||||
sb.AppendLine("## Parameter Analysis");
|
||||
sb.AppendLine();
|
||||
|
||||
if (paramReport.Count == 0)
|
||||
{
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("No parameters are used in this collection.");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Parameter | Type | Used In | Value |");
|
||||
sb.AppendLine("|-----------|------|---------|-------|");
|
||||
|
||||
foreach (var param in paramReport.OrderBy(p => p.ParameterName))
|
||||
{
|
||||
var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}";
|
||||
var value = param.Value?.ToString() ?? "NULL";
|
||||
// PostgreSQL supports both $n positional and :named parameters
|
||||
var paramSyntax = int.TryParse(param.ParameterName, out _)
|
||||
? $"${param.ParameterName}"
|
||||
: $":{param.ParameterName}";
|
||||
sb.AppendLine($"| {paramSyntax} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### Parameter Dependency Diagram");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateParameterDependencyDiagram(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram showing parameter dependencies across queries.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("graph TD");
|
||||
sb.AppendLine();
|
||||
|
||||
var queryBreakdowns = collection.QueryBreakdowns;
|
||||
|
||||
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
||||
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var query in queryBreakdowns)
|
||||
{
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParamNames.Add(param.Name);
|
||||
}
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParamNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
var parameters = allParamNames.OrderBy(p => p).ToList();
|
||||
|
||||
// Create parameter nodes
|
||||
for (int i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
var paramNode = $"param{i}";
|
||||
var paramSyntax = int.TryParse(parameters[i], out _)
|
||||
? $"${parameters[i]}"
|
||||
: $":{parameters[i]}";
|
||||
sb.AppendLine($" {paramNode}[\"{paramSyntax}\"]");
|
||||
sb.AppendLine($" style {paramNode} fill:#e8f5e9");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
// Create query nodes and connections
|
||||
for (int i = 0; i < queryBreakdowns.Count; i++)
|
||||
{
|
||||
var query = queryBreakdowns[i];
|
||||
var queryNode = $"query{i}";
|
||||
var queryType = DetermineQueryType(query);
|
||||
|
||||
sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]");
|
||||
sb.AppendLine($" style {queryNode} fill:#fff3e0");
|
||||
|
||||
// Collect all parameter names used by this query
|
||||
var queryParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
queryParamNames.Add(param.Name);
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
queryParamNames.Add(paramName);
|
||||
}
|
||||
|
||||
// Connect parameters to this query
|
||||
foreach (var paramName in queryParamNames)
|
||||
{
|
||||
var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
||||
if (paramIndex >= 0)
|
||||
{
|
||||
var paramNode = $"param{paramIndex}";
|
||||
sb.AppendLine($" {paramNode} --> {queryNode}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a detailed query composition report.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to report on.</param>
|
||||
/// <returns>Markdown composition report section.</returns>
|
||||
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Query Composition Report");
|
||||
sb.AppendLine();
|
||||
|
||||
var summaries = collection.GetQuerySummaries().ToList();
|
||||
|
||||
for (int i = 0; i < summaries.Count; i++)
|
||||
{
|
||||
var summary = summaries[i];
|
||||
var query = collection.QueryBreakdowns[i];
|
||||
|
||||
AppendQueryCompositionTable(sb, i, summary);
|
||||
AppendQueryParameters(sb, query);
|
||||
AppendQueryCteSections(sb, summary, query);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the query composition table for a single query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, QuerySummary summary)
|
||||
{
|
||||
sb.AppendLine($"### Query #{queryIndex}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Aspect | Present |");
|
||||
sb.AppendLine("|--------|---------|");
|
||||
sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |");
|
||||
sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |");
|
||||
sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |");
|
||||
sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |");
|
||||
sb.AppendLine($"| HAVING Clause | {FormatClausePresence(summary.HasHavingClause)} |");
|
||||
sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |");
|
||||
sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |");
|
||||
sb.AppendLine($"| Columns | {summary.ColumnCount} |");
|
||||
sb.AppendLine($"| Parameters | {summary.ParameterCount} |");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends parameter information for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query)
|
||||
{
|
||||
// Collect all unique parameters from both ParameterList and Parameters dictionary
|
||||
var allParams = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParams[param.Name] = param.Value;
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var param in query.Parameters)
|
||||
{
|
||||
allParams[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
if (allParams.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**Parameters Used:**");
|
||||
sb.AppendLine();
|
||||
foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = allParams[paramName];
|
||||
var paramSyntax = int.TryParse(paramName, out _)
|
||||
? $"${paramName}"
|
||||
: $":{paramName}";
|
||||
sb.AppendLine($"- `{paramSyntax}` = `{value?.ToString() ?? "NULL"}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends CTE section for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCteSections(StringBuilder sb, QuerySummary summary, QueryBreakdown query)
|
||||
{
|
||||
if (!summary.HasCTE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**CTEs Defined:**");
|
||||
sb.AppendLine();
|
||||
foreach (var cte in query.WithClauses)
|
||||
{
|
||||
sb.AppendLine($"- `{cte.TableName}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch execution flow diagram for PostgreSQL.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <param name="includeTransaction">Whether to show transaction wrapping.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
|
||||
int nodeId = 0;
|
||||
|
||||
// Handle empty collection
|
||||
if (collection.QueryBreakdowns.Count == 0)
|
||||
{
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node0[\"BEGIN\"]");
|
||||
sb.AppendLine($" node0 --> node1[\"COMMIT\"]");
|
||||
sb.AppendLine($" node1 --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])");
|
||||
}
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Start node
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"BEGIN\"]");
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId - 1} --> node{nodeId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
}
|
||||
|
||||
// Query nodes
|
||||
for (int i = 0; i < collection.QueryBreakdowns.Count; i++)
|
||||
{
|
||||
if (i < collection.QueryBreakdowns.Count - 1)
|
||||
{
|
||||
// Not the last query - connect to next
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last query - connect to End (or COMMIT if transaction)
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId}[\"COMMIT\"]");
|
||||
sb.AppendLine($" node{nodeId} --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter type name from a parameter value using PostgreSQL types.
|
||||
/// </summary>
|
||||
private static string GetParameterType(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "NULL",
|
||||
bool => "BOOLEAN",
|
||||
byte or short => "SMALLINT",
|
||||
int => "INTEGER",
|
||||
long => "BIGINT",
|
||||
float => "REAL",
|
||||
double => "DOUBLE PRECISION",
|
||||
decimal => "NUMERIC",
|
||||
string => "TEXT",
|
||||
DateTime => "TIMESTAMP",
|
||||
_ => "UNKNOWN"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes special Markdown characters.
|
||||
/// </summary>
|
||||
private static string EscapeMarkdown(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("|", "\\|")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats clause presence as Yes/No with checkmark/cross.
|
||||
/// </summary>
|
||||
private static string FormatClausePresence(bool isPresent)
|
||||
=> isPresent ? "✓ Yes" : "✗ No";
|
||||
|
||||
/// <summary>
|
||||
/// Determines the query type from a QueryBreakdown.
|
||||
/// </summary>
|
||||
private static string DetermineQueryType(QueryBreakdown query)
|
||||
{
|
||||
var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause);
|
||||
if (hasSelect)
|
||||
{
|
||||
return "SELECT";
|
||||
}
|
||||
|
||||
var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause);
|
||||
return hasFrom ? "FROM" : "QUERY";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Strata.SqlTools.Breakdowns.PostgreSql;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagram markdown from PostgreSQL SQL QueryBreakdown objects.
|
||||
/// Creates flowchart visualizations showing the query structure and flow.
|
||||
/// </summary>
|
||||
public class QueryBreakdownGenerator
|
||||
{
|
||||
private readonly SqlServer.QueryBreakdownGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the QueryBreakdownGenerator class.
|
||||
/// </summary>
|
||||
public QueryBreakdownGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.QueryBreakdownGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from a PostgreSQL QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The PostgreSQL QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(QueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
// Since PostgreSql.QueryBreakdown inherits from SqlServer.QueryBreakdown,
|
||||
// we can use the base generator which works with the shared properties
|
||||
return _baseGenerator.GenerateMermaidDiagram(queryBreakdown, title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagrams for PostgreSQL SQL statements, including sequence diagrams
|
||||
/// for statement execution flow and entity-relationship diagrams.
|
||||
/// </summary>
|
||||
public class SqlStatementGenerator
|
||||
{
|
||||
private readonly SqlServer.SqlStatementGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SqlStatementGenerator class.
|
||||
/// </summary>
|
||||
public SqlStatementGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.SqlStatementGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid sequence diagram showing PostgreSQL SQL statement execution flow.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The PostgreSQL SQL breakdown object.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid sequence diagram markdown.</returns>
|
||||
public string GenerateSequenceDiagram(SqlBreakdownBase sqlBreakdown, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateSequenceDiagram(sqlBreakdown, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid entity-relationship diagram from table names.
|
||||
/// </summary>
|
||||
/// <param name="tables">Collection of table names to include in the diagram.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid ER diagram markdown.</returns>
|
||||
public string GenerateEntityRelationshipDiagram(IEnumerable<string> tables, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateEntityRelationshipDiagram(tables, title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
# Strata.SqlTools.Markdown
|
||||
|
||||
Markdown documentation generation for SQL queries and expressions from Strata.SqlTools.
|
||||
|
||||
## Overview
|
||||
|
||||
This library provides tools to generate markdown documentation and Mermaid diagrams from SQL query breakdowns and expression trees. It's designed to help document SQL queries and their structure in a human-readable format.
|
||||
|
||||
## Features
|
||||
|
||||
### SqlServer Folder - Mermaid Diagram Generation
|
||||
|
||||
#### QueryBreakdownGenerator
|
||||
Generates Mermaid flowchart diagrams from SQL `QueryBreakdown` objects, visualizing:
|
||||
- WITH clauses (Common Table Expressions)
|
||||
- SELECT, FROM, WHERE clauses
|
||||
- GROUP BY, HAVING, ORDER BY clauses
|
||||
- Setup and Finish clauses
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using Strata.SqlTools.Markdown.SqlServer;
|
||||
|
||||
var query = QueryBreakdown.Parse(@"
|
||||
SELECT u.ID, u.Name, COUNT(o.OrderID) as OrderCount
|
||||
FROM Users u
|
||||
JOIN Orders o ON u.ID = o.UserID
|
||||
WHERE u.Active = 1
|
||||
GROUP BY u.ID, u.Name
|
||||
HAVING COUNT(o.OrderID) > 5
|
||||
ORDER BY OrderCount DESC
|
||||
");
|
||||
|
||||
var generator = new QueryBreakdownGenerator();
|
||||
string markdown = generator.GenerateMermaidDiagram(query, "User Orders Query");
|
||||
|
||||
// Output the markdown to a file or display
|
||||
Console.WriteLine(markdown);
|
||||
```
|
||||
|
||||
#### SqlStatementGenerator
|
||||
Generates Mermaid sequence diagrams showing SQL statement execution flow and entity-relationship diagrams.
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
var seqGenerator = new SqlStatementGenerator();
|
||||
string sequenceDiagram = seqGenerator.GenerateSequenceDiagram(sqlBreakdown, "Query Execution Flow");
|
||||
|
||||
// Generate ER diagram for tables
|
||||
var tables = new[] { "Users", "Orders", "OrderDetails" };
|
||||
string erDiagram = seqGenerator.GenerateEntityRelationshipDiagram(tables, "Database Schema");
|
||||
```
|
||||
|
||||
### Snowflake Folder - Snowflake SQL Support
|
||||
|
||||
The library fully supports Snowflake SQL syntax, including Snowflake-specific features like:
|
||||
- `:parameter` syntax (in addition to `@parameter`)
|
||||
- Double-quoted identifiers `"identifier"`
|
||||
- QUALIFY clauses for window functions
|
||||
- Type casting with `::` operator
|
||||
- JSON path notation with `:` accessor
|
||||
|
||||
#### QueryBreakdownGenerator (Snowflake)
|
||||
Generates Mermaid flowchart diagrams from Snowflake `QueryBreakdown` objects.
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
using Strata.SqlTools.Markdown.Snowflake;
|
||||
|
||||
// Parse Snowflake SQL with :parameter syntax
|
||||
var query = QueryBreakdown.Parse(@"
|
||||
WITH ACTIVE_USERS AS (
|
||||
SELECT USER_ID, USER_NAME, EMAIL
|
||||
FROM USERS
|
||||
WHERE STATUS = :status AND REGION = :region
|
||||
)
|
||||
SELECT
|
||||
AU.USER_ID,
|
||||
AU.USER_NAME,
|
||||
COUNT(O.ORDER_ID) AS ORDER_COUNT,
|
||||
SUM(O.AMOUNT):: DECIMAL(10,2) AS TOTAL_AMOUNT
|
||||
FROM ACTIVE_USERS AU
|
||||
LEFT JOIN ORDERS O ON AU.USER_ID = O.USER_ID
|
||||
WHERE O.ORDER_DATE >= :startDate
|
||||
GROUP BY AU.USER_ID, AU.USER_NAME
|
||||
HAVING COUNT(O.ORDER_ID) > 0
|
||||
ORDER BY TOTAL_AMOUNT DESC
|
||||
", isMicrosoftSql: false);
|
||||
|
||||
var generator = new QueryBreakdownGenerator();
|
||||
string markdown = generator.GenerateMermaidDiagram(query, "Snowflake User Orders Analysis");
|
||||
|
||||
Console.WriteLine(markdown);
|
||||
```
|
||||
|
||||
#### SqlStatementGenerator (Snowflake)
|
||||
Generates sequence and ER diagrams for Snowflake SQL statements.
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
var seqGenerator = new SqlStatementGenerator();
|
||||
|
||||
// Generate sequence diagram for Snowflake query flow
|
||||
string sequenceDiagram = seqGenerator.GenerateSequenceDiagram(snowflakeQuery, "Snowflake Query Flow");
|
||||
|
||||
// Generate ER diagram for Snowflake tables (typically uppercase)
|
||||
var tables = new[] { "CUSTOMERS", "ORDERS", "ORDER_ITEMS", "PRODUCTS" };
|
||||
string erDiagram = seqGenerator.GenerateEntityRelationshipDiagram(tables, "Snowflake Schema");
|
||||
```
|
||||
|
||||
### Expressions Folder - Expression Documentation
|
||||
|
||||
#### ExpressionGenerator
|
||||
Generates comprehensive markdown documentation for SQL expression trees with:
|
||||
- Hierarchical structure visualization
|
||||
- Type information
|
||||
- Mermaid tree diagrams
|
||||
- Mathematical notation using LaTeX (GitHub compatible)
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.Markdown.Expressions;
|
||||
|
||||
// Build an expression
|
||||
Expression quantity = new ColumnExpression<TableSource>(tableSource, "Quantity");
|
||||
Expression unitPrice = new ColumnExpression<TableSource>(tableSource, "UnitPrice");
|
||||
Expression discount = new ColumnExpression<TableSource>(tableSource, "Discount");
|
||||
|
||||
var totalExpression = (quantity * unitPrice) * (1 - discount);
|
||||
|
||||
var generator = new ExpressionGenerator();
|
||||
string markdown = generator.GenerateMarkdown(totalExpression, "Order Line Total Calculation");
|
||||
|
||||
// Output includes:
|
||||
// - Expression structure tree
|
||||
// - Type information
|
||||
// - Mermaid diagram visualization
|
||||
// - Mathematical expression in LaTeX format
|
||||
Console.WriteLine(markdown);
|
||||
|
||||
// Or generate just the mathematical expression
|
||||
string mathExpr = generator.GenerateMathematicalExpression(totalExpression);
|
||||
// Produces: $$(Quantity \times UnitPrice) \times (1 - Discount)$$
|
||||
|
||||
// For inline math notation
|
||||
string inlineMath = generator.GenerateMathematicalExpression(totalExpression, inline: true);
|
||||
// Produces: $(Quantity \times UnitPrice) \times (1 - Discount)$
|
||||
|
||||
// For raw LaTeX expressions (e.g., mathematical formulas)
|
||||
var cauchySchwarz = @"\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)";
|
||||
string dollarFormat = generator.GenerateRawMathematicalExpression(cauchySchwarz);
|
||||
// Produces: $$
|
||||
// \left( \sum_{k=1}^n a_k b_k \right)^2 \leq ...
|
||||
// $$
|
||||
|
||||
string mathCodeFence = generator.GenerateRawMathematicalExpression(cauchySchwarz, format: "math");
|
||||
// Produces: ```math
|
||||
// \left( \sum_{k=1}^n a_k b_k \right)^2 \leq ...
|
||||
// ```
|
||||
```
|
||||
|
||||
**Mathematical Notation Features:**
|
||||
- Arithmetic operators: `+`, `-`, `×` (`\times`), `÷` (`\div`), `mod` (`\bmod`)
|
||||
- Comparison operators: `=`, `≠` (`\neq`), `<`, `>`, `≤` (`\leq`), `≥` (`\geq`)
|
||||
- Logical operators: `∧` (`\land`), `∨` (`\lor`), `¬` (`\neg`)
|
||||
- Functions: `SUM` (`\sum`), `MIN` (`\min`), `MAX` (`\max`), `|x|` (ABS), `√` (`\sqrt`), powers, etc.
|
||||
- Set operations: `∈` for BETWEEN and IN expressions
|
||||
- Case expressions using piecewise notation (`\begin{cases}`)
|
||||
|
||||
|
||||
#### SimpleExpressionGenerator
|
||||
Generates simplified, readable markdown documentation for expressions with:
|
||||
- SQL representation
|
||||
- Type information
|
||||
- Human-readable descriptions
|
||||
- Comparison tables for multiple expressions
|
||||
- Bulleted lists
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
var simpleGenerator = new SimpleExpressionGenerator();
|
||||
|
||||
// Generate simple markdown for a single expression
|
||||
string simpleMarkdown = simpleGenerator.GenerateMarkdown(expression, "Price Filter");
|
||||
|
||||
// Generate comparison table for multiple expressions
|
||||
var expressions = new Dictionary<string, Expression>
|
||||
{
|
||||
["Basic Filter"] = status == "Active",
|
||||
["Date Filter"] = orderDate > new DateTime(2024, 1, 1),
|
||||
["Complex Filter"] = (quantity > 10) & (price < 100)
|
||||
};
|
||||
|
||||
string comparisonTable = simpleGenerator.GenerateComparisonTable(expressions, "Filter Expressions");
|
||||
|
||||
// Generate bullet list
|
||||
var expressionList = new List<Expression> { expr1, expr2, expr3 };
|
||||
string bulletList = simpleGenerator.GenerateBulletList(expressionList, "Common Filters");
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Add a reference to this project in your .csproj file:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.Markdown\Strata.SqlTools.Markdown.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Strata.SqlTools - Core SQL utilities library
|
||||
- Strata.SqlTools.SqlServer - SQL Server specific implementations
|
||||
- Strata.SqlTools.Snowflake - Snowflake specific implementations
|
||||
- .NET 9.0 or later
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **Documentation Generation**: Automatically generate documentation for complex SQL queries
|
||||
2. **Code Review**: Visualize query structure for easier code reviews
|
||||
3. **Learning Tool**: Help developers understand complex SQL queries through visual diagrams
|
||||
4. **Query Analysis**: Analyze query patterns and structures
|
||||
5. **API Documentation**: Document SQL expressions used in query builders
|
||||
|
||||
## Output Examples
|
||||
|
||||
### Mermaid Flowchart
|
||||
The `QueryBreakdownGenerator` produces flowcharts like:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([Query Start]) --> Node1
|
||||
Node1["SELECT<br/>u.ID, u.Name, COUNT(o.OrderID)"]
|
||||
Node1 --> Node2
|
||||
Node2["FROM<br/>Users u JOIN Orders o"]
|
||||
Node2 --> Node3
|
||||
Node3{"WHERE<br/>u.Active = 1"}
|
||||
Node3 --> Node4
|
||||
Node4["GROUP BY<br/>u.ID, u.Name"]
|
||||
Node4 --> Node5
|
||||
Node5{"HAVING<br/>COUNT(o.OrderID) > 5"}
|
||||
Node5 --> Node6
|
||||
Node6["ORDER BY<br/>OrderCount DESC"]
|
||||
Node6 --> End([Query End])
|
||||
```
|
||||
|
||||
### Expression Documentation
|
||||
|
||||
#### Comprehensive Expression Markdown (ExpressionGenerator)
|
||||
|
||||
The `ExpressionGenerator` produces detailed documentation including structure, type info, diagrams, and mathematical notation:
|
||||
|
||||
```markdown
|
||||
# Order Line Total Calculation
|
||||
|
||||
## Expression Structure
|
||||
- **Type**: ArithmeticExpression
|
||||
- **Operator**: Multiply (*)
|
||||
- **Left Expression**: ArithmeticExpression (Quantity * UnitPrice)
|
||||
- **Right Expression**: ArithmeticExpression (1 - Discount)
|
||||
|
||||
## Mermaid Diagram
|
||||
```mermaid
|
||||
graph TD
|
||||
Root["* (Multiply)"]
|
||||
Root --> Left["* (Multiply)"]
|
||||
Root --> Right["- (Subtract)"]
|
||||
Left --> LeftLeft["Quantity (Column)"]
|
||||
Left --> LeftRight["UnitPrice (Column)"]
|
||||
Right --> RightLeft["1 (Constant)"]
|
||||
Right --> RightRight["Discount (Column)"]
|
||||
```
|
||||
|
||||
## Mathematical Expression
|
||||
$$(Quantity \times UnitPrice) \times (1 - Discount)$$
|
||||
```
|
||||
|
||||
#### Simple Expression Markdown (SimpleExpressionGenerator)
|
||||
|
||||
The `SimpleExpressionGenerator` produces concise, readable output:
|
||||
|
||||
**Single Expression:**
|
||||
```markdown
|
||||
# Price Filter
|
||||
|
||||
**Expression Type**: ComparisonExpression
|
||||
|
||||
**SQL Representation**:
|
||||
```sql
|
||||
UnitPrice < 100
|
||||
```
|
||||
|
||||
**Description**: Filters records where UnitPrice is less than 100
|
||||
```
|
||||
|
||||
**Comparison Table:**
|
||||
```markdown
|
||||
# Filter Expressions Comparison
|
||||
|
||||
| Name | Expression Type | SQL Representation |
|
||||
|------|----------------|-------------------|
|
||||
| Basic Filter | ComparisonExpression | `Status = 'Active'` |
|
||||
| Date Filter | ComparisonExpression | `OrderDate > '2024-01-01'` |
|
||||
| Complex Filter | LogicalExpression | `(Quantity > 10) AND (Price < 100)` |
|
||||
```
|
||||
|
||||
**Bullet List:**
|
||||
```markdown
|
||||
# Common Filters
|
||||
|
||||
- **Status = 'Active'** (ComparisonExpression)
|
||||
- **OrderDate > '2024-01-01'** (ComparisonExpression)
|
||||
- **(Quantity > 10) AND (Price < 100)** (LogicalExpression)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please ensure all code follows the existing patterns and includes appropriate documentation.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Copyright © Strata Decision Technology 2024-2026
|
||||
@@ -0,0 +1,459 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Markdown documentation from QueryBreakdownCollection objects for Snowflake.
|
||||
/// Creates comprehensive reports including collection summaries, parameter analysis, and batch flow visualization
|
||||
/// with Snowflake-specific features.
|
||||
/// </summary>
|
||||
public static class QueryBreakdownCollectionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a comprehensive collection report in Markdown format with Snowflake-specific information.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to document.</param>
|
||||
/// <param name="title">Optional title for the report.</param>
|
||||
/// <returns>A string containing the Markdown documentation.</returns>
|
||||
public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Collection Summary
|
||||
sb.Append(GenerateCollectionSummary(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Snowflake Features Analysis
|
||||
sb.Append(GenerateSnowflakeFeaturesAnalysis(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Parameter Analysis
|
||||
sb.Append(GenerateParameterAnalysis(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Query Composition Report
|
||||
sb.Append(GenerateQueryCompositionReport(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary section for the collection.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to summarize.</param>
|
||||
/// <returns>Markdown summary section.</returns>
|
||||
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Collection Summary");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("| Metric | Value |");
|
||||
sb.AppendLine("|--------|-------|");
|
||||
sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |");
|
||||
sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |");
|
||||
sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |");
|
||||
sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Snowflake-specific features analysis section.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
|
||||
/// <returns>Markdown Snowflake features section.</returns>
|
||||
public static string GenerateSnowflakeFeaturesAnalysis(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Snowflake Features");
|
||||
sb.AppendLine();
|
||||
|
||||
var queriesWithStages = collection.WhereUseStageReference().ToList();
|
||||
var queriesWithSemiStructured = collection.WhereUseSemiStructuredData().ToList();
|
||||
|
||||
sb.AppendLine("| Feature | Used | Count |");
|
||||
sb.AppendLine("|---------|------|-------|");
|
||||
sb.AppendLine($"| Stage References | {FormatFeaturePresence(queriesWithStages.Count > 0)} | {queriesWithStages.Count} |");
|
||||
sb.AppendLine($"| Semi-Structured Data | {FormatFeaturePresence(queriesWithSemiStructured.Count > 0)} | {queriesWithSemiStructured.Count} |");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a parameter analysis report with Snowflake parameter syntax support.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
|
||||
/// <returns>Markdown parameter analysis section.</returns>
|
||||
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var paramReport = collection.GetParameterUsageReport().ToList();
|
||||
|
||||
sb.AppendLine("## Parameter Analysis");
|
||||
sb.AppendLine();
|
||||
|
||||
if (paramReport.Count == 0)
|
||||
{
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("No parameters are used in this collection.");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Parameter | Type | Used In | Value |");
|
||||
sb.AppendLine("|-----------|------|---------|-------|");
|
||||
|
||||
foreach (var param in paramReport.OrderBy(p => p.ParameterName))
|
||||
{
|
||||
var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}";
|
||||
var value = param.Value?.ToString() ?? "NULL";
|
||||
// Snowflake supports both : and @ syntax for parameters
|
||||
sb.AppendLine($"| :{param.ParameterName} / @{param.ParameterName} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### Parameter Dependency Diagram");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateParameterDependencyDiagram(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram showing parameter dependencies across queries.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("graph TD");
|
||||
sb.AppendLine();
|
||||
|
||||
var queryBreakdowns = collection.QueryBreakdowns;
|
||||
|
||||
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
||||
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var query in queryBreakdowns)
|
||||
{
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParamNames.Add(param.Name);
|
||||
}
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParamNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
var parameters = allParamNames.OrderBy(p => p).ToList();
|
||||
|
||||
// Create parameter nodes
|
||||
for (int i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
var paramNode = $"param{i}";
|
||||
sb.AppendLine($" {paramNode}[\":{parameters[i]}\"]");
|
||||
sb.AppendLine($" style {paramNode} fill:#e0f2f1");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
// Create query nodes and connections
|
||||
for (int i = 0; i < queryBreakdowns.Count; i++)
|
||||
{
|
||||
var query = queryBreakdowns[i];
|
||||
var queryNode = $"query{i}";
|
||||
var queryType = DetermineQueryType(query);
|
||||
|
||||
sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]");
|
||||
sb.AppendLine($" style {queryNode} fill:#f1f8e9");
|
||||
|
||||
// Collect all parameter names used by this query
|
||||
var queryParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
queryParamNames.Add(param.Name);
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
queryParamNames.Add(paramName);
|
||||
}
|
||||
|
||||
// Connect parameters to this query
|
||||
foreach (var paramName in queryParamNames)
|
||||
{
|
||||
var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
||||
if (paramIndex >= 0)
|
||||
{
|
||||
var paramNode = $"param{paramIndex}";
|
||||
sb.AppendLine($" {paramNode} --> {queryNode}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a detailed query composition report with Snowflake-specific information.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to report on.</param>
|
||||
/// <returns>Markdown composition report section.</returns>
|
||||
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Query Composition Report");
|
||||
sb.AppendLine();
|
||||
|
||||
var summaries = collection.GetQuerySummaries().ToList();
|
||||
var stageQueries = collection.WhereUseStageReference().ToList();
|
||||
var semiStructured = collection.WhereUseSemiStructuredData().ToList();
|
||||
|
||||
for (int i = 0; i < summaries.Count; i++)
|
||||
{
|
||||
var summary = summaries[i];
|
||||
var query = collection.QueryBreakdowns[i];
|
||||
|
||||
AppendQueryCompositionTable(sb, i, summary);
|
||||
AppendQueryParameters(sb, query);
|
||||
AppendQueryCteSections(sb, summary, query);
|
||||
AppendSnowflakeFeatures(sb, query, stageQueries, semiStructured);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the query composition table for a single query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, SnowflakeQueryAnalysis summary)
|
||||
{
|
||||
sb.AppendLine($"### Query #{queryIndex}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Aspect | Present |");
|
||||
sb.AppendLine("|--------|---------|");
|
||||
sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |");
|
||||
sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |");
|
||||
sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |");
|
||||
sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |");
|
||||
sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |");
|
||||
sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |");
|
||||
sb.AppendLine($"| Columns | {summary.ColumnCount} |");
|
||||
sb.AppendLine($"| Parameters | {summary.ParameterCount} |");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends parameter information for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query)
|
||||
{
|
||||
// Collect all unique parameters from both ParameterList and Parameters dictionary
|
||||
var allParams = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParams[param.Name] = param.Value;
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var param in query.Parameters)
|
||||
{
|
||||
allParams[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
if (allParams.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**Parameters Used:**");
|
||||
sb.AppendLine();
|
||||
foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = allParams[paramName];
|
||||
sb.AppendLine($"- `:{paramName}` = `{value?.ToString() ?? "NULL"}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends CTE section for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCteSections(StringBuilder sb, SnowflakeQueryAnalysis summary, QueryBreakdown query)
|
||||
{
|
||||
if (!summary.HasCTE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**CTEs Defined:**");
|
||||
sb.AppendLine();
|
||||
foreach (var cte in query.WithClauses)
|
||||
{
|
||||
sb.AppendLine($"- `{cte.TableName}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends Snowflake-specific feature information for a query.
|
||||
/// </summary>
|
||||
private static void AppendSnowflakeFeatures(StringBuilder sb, QueryBreakdown query, List<QueryBreakdown> stageQueries, List<QueryBreakdown> semiStructured)
|
||||
{
|
||||
if (stageQueries.Contains(query))
|
||||
{
|
||||
sb.AppendLine("**Snowflake Features:** Stage References");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (semiStructured.Contains(query))
|
||||
{
|
||||
sb.AppendLine("**Snowflake Features:** Semi-Structured Data");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats clause presence as Yes/No with checkmark/cross.
|
||||
/// </summary>
|
||||
private static string FormatClausePresence(bool isPresent)
|
||||
=> isPresent ? "✓ Yes" : "✗ No";
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch execution flow diagram for Snowflake.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <param name="includeSessionSetup">Whether to show session setup statements.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeSessionSetup = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
|
||||
// Handle empty collection
|
||||
if (collection.QueryBreakdowns.Count == 0)
|
||||
{
|
||||
if (includeSessionSetup)
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node0[\"Session Setup\"]");
|
||||
sb.AppendLine($" node0 --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])");
|
||||
}
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
int nodeId = 0;
|
||||
|
||||
// Start node
|
||||
if (includeSessionSetup)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Session Setup\"]");
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId - 1} --> node{nodeId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
}
|
||||
|
||||
// Query nodes
|
||||
for (int i = 0; i < collection.QueryBreakdowns.Count; i++)
|
||||
{
|
||||
if (i < collection.QueryBreakdowns.Count - 1)
|
||||
{
|
||||
// Not the last query - connect to next
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last query - connect to End
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter type name from a parameter value.
|
||||
/// </summary>
|
||||
private static string GetParameterType(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "NULL",
|
||||
bool => "BOOLEAN",
|
||||
byte or short or int or long => "NUMBER",
|
||||
float or double or decimal => "FLOAT",
|
||||
string => "VARCHAR",
|
||||
DateTime => "TIMESTAMP",
|
||||
_ => "VARIANT"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes special Markdown characters.
|
||||
/// </summary>
|
||||
private static string EscapeMarkdown(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("|", "\\|")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats feature presence as Yes/No with checkmark/cross.
|
||||
/// </summary>
|
||||
private static string FormatFeaturePresence(bool isPresent)
|
||||
=> isPresent ? "✓ Yes" : "✗ No";
|
||||
|
||||
/// <summary>
|
||||
/// Determines the query type from a QueryBreakdown.
|
||||
/// </summary>
|
||||
private static string DetermineQueryType(QueryBreakdown query)
|
||||
{
|
||||
var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause);
|
||||
if (hasSelect)
|
||||
{
|
||||
return "SELECT";
|
||||
}
|
||||
|
||||
var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause);
|
||||
return hasFrom ? "FROM" : "QUERY";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagram markdown from Snowflake SQL QueryBreakdown objects.
|
||||
/// Creates flowchart visualizations showing the query structure and flow.
|
||||
/// </summary>
|
||||
public class QueryBreakdownGenerator
|
||||
{
|
||||
private readonly SqlServer.QueryBreakdownGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the QueryBreakdownGenerator class.
|
||||
/// </summary>
|
||||
public QueryBreakdownGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.QueryBreakdownGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from a Snowflake QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The Snowflake QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(QueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
// Since Snowflake.QueryBreakdown inherits from SqlServer.QueryBreakdown,
|
||||
// we can use the base generator which works with the shared properties
|
||||
return _baseGenerator.GenerateMermaidDiagram(queryBreakdown, title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagrams for Snowflake SQL statements, including sequence diagrams
|
||||
/// for statement execution flow and entity-relationship diagrams.
|
||||
/// </summary>
|
||||
public class SqlStatementGenerator
|
||||
{
|
||||
private readonly SqlServer.SqlStatementGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SqlStatementGenerator class.
|
||||
/// </summary>
|
||||
public SqlStatementGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.SqlStatementGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid sequence diagram showing Snowflake SQL statement execution flow.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The Snowflake SQL breakdown object.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid sequence diagram markdown.</returns>
|
||||
public string GenerateSequenceDiagram(SqlBreakdownBase sqlBreakdown, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateSequenceDiagram(sqlBreakdown, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid entity-relationship diagram from table names.
|
||||
/// </summary>
|
||||
/// <param name="tables">Collection of table names to include in the diagram.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid ER diagram markdown.</returns>
|
||||
public string GenerateEntityRelationshipDiagram(IEnumerable<string> tables, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateEntityRelationshipDiagram(tables, title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Markdown documentation from QueryBreakdownCollection objects.
|
||||
/// Creates comprehensive reports including collection summaries, parameter analysis, and batch flow visualization.
|
||||
/// </summary>
|
||||
public static class QueryBreakdownCollectionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a comprehensive collection report in Markdown format.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to document.</param>
|
||||
/// <param name="title">Optional title for the report.</param>
|
||||
/// <returns>A string containing the Markdown documentation.</returns>
|
||||
public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Collection Summary
|
||||
sb.Append(GenerateCollectionSummary(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Parameter Analysis
|
||||
sb.Append(GenerateParameterAnalysis(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Query Composition Report
|
||||
sb.Append(GenerateQueryCompositionReport(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary section for the collection.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to summarize.</param>
|
||||
/// <returns>Markdown summary section.</returns>
|
||||
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Collection Summary");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("| Metric | Value |");
|
||||
sb.AppendLine("|--------|-------|");
|
||||
sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |");
|
||||
sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |");
|
||||
sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |");
|
||||
sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a parameter analysis report.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
|
||||
/// <returns>Markdown parameter analysis section.</returns>
|
||||
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var paramReport = collection.GetParameterUsageReport().ToList();
|
||||
|
||||
sb.AppendLine("## Parameter Analysis");
|
||||
sb.AppendLine();
|
||||
|
||||
if (paramReport.Count == 0)
|
||||
{
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("No parameters are used in this collection.");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Parameter | Type | Used In | Value |");
|
||||
sb.AppendLine("|-----------|------|---------|-------|");
|
||||
|
||||
foreach (var param in paramReport.OrderBy(p => p.ParameterName))
|
||||
{
|
||||
var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}";
|
||||
var value = param.Value?.ToString() ?? "NULL";
|
||||
|
||||
sb.AppendLine($"| @{param.ParameterName} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### Parameter Dependency Diagram");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateParameterDependencyDiagram(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram showing parameter dependencies across queries.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("graph TD");
|
||||
sb.AppendLine();
|
||||
|
||||
var queryBreakdowns = collection.QueryBreakdowns;
|
||||
|
||||
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
||||
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var query in queryBreakdowns)
|
||||
{
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParamNames.Add(param.Name);
|
||||
}
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParamNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
var parameters = allParamNames.OrderBy(p => p).ToList();
|
||||
|
||||
// Create parameter nodes
|
||||
for (int i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
var paramNode = $"param{i}";
|
||||
sb.AppendLine($" {paramNode}[\"@{parameters[i]}\"]");
|
||||
sb.AppendLine($" style {paramNode} fill:#e1f5ff");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
// Create query nodes and connections
|
||||
for (int i = 0; i < queryBreakdowns.Count; i++)
|
||||
{
|
||||
var query = queryBreakdowns[i];
|
||||
var queryNode = $"query{i}";
|
||||
var queryType = DetermineQueryType(query);
|
||||
|
||||
sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]");
|
||||
sb.AppendLine($" style {queryNode} fill:#f3e5f5");
|
||||
|
||||
// Collect all parameter names used by this query
|
||||
var queryParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
queryParamNames.Add(param.Name);
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
queryParamNames.Add(paramName);
|
||||
}
|
||||
|
||||
// Connect parameters to this query
|
||||
foreach (var paramName in queryParamNames)
|
||||
{
|
||||
var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
||||
if (paramIndex >= 0)
|
||||
{
|
||||
var paramNode = $"param{paramIndex}";
|
||||
sb.AppendLine($" {paramNode} --> {queryNode}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a detailed query composition report.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to report on.</param>
|
||||
/// <returns>Markdown composition report section.</returns>
|
||||
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Query Composition Report");
|
||||
sb.AppendLine();
|
||||
|
||||
var summaries = collection.GetQuerySummaries().ToList();
|
||||
|
||||
for (int i = 0; i < summaries.Count; i++)
|
||||
{
|
||||
var summary = summaries[i];
|
||||
var query = collection.QueryBreakdowns[i];
|
||||
|
||||
AppendQueryCompositionTable(sb, i, summary);
|
||||
AppendQueryParameters(sb, query);
|
||||
AppendQueryCteSections(sb, summary, query);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the query composition table for a single query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, QuerySummary summary)
|
||||
{
|
||||
sb.AppendLine($"### Query #{queryIndex}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Aspect | Present |");
|
||||
sb.AppendLine("|--------|---------|");
|
||||
sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |");
|
||||
sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |");
|
||||
sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |");
|
||||
sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |");
|
||||
sb.AppendLine($"| HAVING Clause | {FormatClausePresence(summary.HasHavingClause)} |");
|
||||
sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |");
|
||||
sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |");
|
||||
sb.AppendLine($"| Columns | {summary.ColumnCount} |");
|
||||
sb.AppendLine($"| Parameters | {summary.ParameterCount} |");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends parameter information for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query)
|
||||
{
|
||||
// Collect all unique parameters from both ParameterList and Parameters dictionary
|
||||
var allParams = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParams[param.Name] = param.Value;
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var param in query.Parameters)
|
||||
{
|
||||
allParams[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
if (allParams.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**Parameters Used:**");
|
||||
sb.AppendLine();
|
||||
foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = allParams[paramName];
|
||||
sb.AppendLine($"- `@{paramName}` = `{value?.ToString() ?? "NULL"}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends CTE section for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCteSections(StringBuilder sb, QuerySummary summary, QueryBreakdown query)
|
||||
{
|
||||
if (!summary.HasCTE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**CTEs Defined:**");
|
||||
sb.AppendLine();
|
||||
foreach (var cte in query.WithClauses)
|
||||
{
|
||||
sb.AppendLine($"- `{cte.TableName}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch execution flow diagram.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <param name="includeTransaction">Whether to show transaction wrapping.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
|
||||
int nodeId = 0;
|
||||
|
||||
// Handle empty collection
|
||||
if (collection.QueryBreakdowns.Count == 0)
|
||||
{
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node0[\"BEGIN TRANSACTION\"]");
|
||||
sb.AppendLine($" node0 --> node1[\"COMMIT TRANSACTION\"]");
|
||||
sb.AppendLine($" node1 --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])");
|
||||
}
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Start node
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"BEGIN TRANSACTION\"]");
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId - 1} --> node{nodeId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
}
|
||||
|
||||
// Query nodes
|
||||
for (int i = 0; i < collection.QueryBreakdowns.Count; i++)
|
||||
{
|
||||
if (i < collection.QueryBreakdowns.Count - 1)
|
||||
{
|
||||
// Not the last query - connect to next
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last query - connect to End (or COMMIT if transaction)
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId}[\"COMMIT TRANSACTION\"]");
|
||||
sb.AppendLine($" node{nodeId} --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter type name from a parameter value.
|
||||
/// </summary>
|
||||
private static string GetParameterType(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "NULL",
|
||||
bool => "BIT",
|
||||
byte => "TINYINT",
|
||||
short => "SMALLINT",
|
||||
int => "INT",
|
||||
long => "BIGINT",
|
||||
float => "REAL",
|
||||
double => "FLOAT",
|
||||
decimal => "DECIMAL",
|
||||
string => "NVARCHAR",
|
||||
DateTime => "DATETIME2",
|
||||
_ => "VARIANT"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes special Markdown characters.
|
||||
/// </summary>
|
||||
private static string EscapeMarkdown(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("|", "\\|")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats clause presence as Yes/No with checkmark/cross.
|
||||
/// </summary>
|
||||
private static string FormatClausePresence(bool isPresent)
|
||||
=> isPresent ? "✓ Yes" : "✗ No";
|
||||
|
||||
/// <summary>
|
||||
/// Determines the query type from a QueryBreakdown.
|
||||
/// </summary>
|
||||
private static string DetermineQueryType(QueryBreakdown query)
|
||||
{
|
||||
var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause);
|
||||
if (hasSelect)
|
||||
{
|
||||
return "SELECT";
|
||||
}
|
||||
|
||||
var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause);
|
||||
return hasFrom ? "FROM" : "QUERY";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagram markdown from SQL QueryBreakdown objects.
|
||||
/// Creates flowchart visualizations showing the query structure and flow.
|
||||
/// </summary>
|
||||
public class QueryBreakdownGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from a QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(QueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Add title if provided
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Start Mermaid flowchart
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
|
||||
int nodeId = 1;
|
||||
|
||||
// Start node
|
||||
sb.AppendLine($" Start([Query Start]) --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
|
||||
// WITH clause (CTE)
|
||||
if (queryBreakdown.IsUsingWithClause)
|
||||
{
|
||||
sb.AppendLine($" Node{nodeId}[\"WITH Clause<br/>Common Table Expressions\"]");
|
||||
foreach (var withClause in queryBreakdown.WithClauses)
|
||||
{
|
||||
sb.AppendLine($" Node{nodeId} --> CTE{nodeId}[\"{EscapeMermaidText(withClause.TableName)}\"]");
|
||||
nodeId++;
|
||||
}
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// SELECT clause
|
||||
if (!string.IsNullOrEmpty(queryBreakdown.SelectClause.Clause))
|
||||
{
|
||||
var selectText = TruncateText(queryBreakdown.SelectClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}[\"SELECT<br/>{EscapeMermaidText(selectText)}\"]");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// FROM clause
|
||||
if (queryBreakdown.IsUsingFromClause && !string.IsNullOrWhiteSpace(queryBreakdown.FromClause?.Clause))
|
||||
{
|
||||
var fromText = TruncateText(queryBreakdown.FromClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}[\"FROM<br/>{EscapeMermaidText(fromText)}\"]");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// WHERE clause
|
||||
if (queryBreakdown.IsUsingWhereClause && !string.IsNullOrWhiteSpace(queryBreakdown.WhereClause?.Clause))
|
||||
{
|
||||
var whereText = TruncateText(queryBreakdown.WhereClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}{{\"WHERE<br/>{EscapeMermaidText(whereText)}\"}}");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// GROUP BY clause
|
||||
if (queryBreakdown.IsUsingGroupByClause && !string.IsNullOrWhiteSpace(queryBreakdown.GroupByClause?.Clause))
|
||||
{
|
||||
var groupByText = TruncateText(queryBreakdown.GroupByClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}[\"GROUP BY<br/>{EscapeMermaidText(groupByText)}\"]");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// HAVING clause
|
||||
if (queryBreakdown.IsUsingHavingClause && !string.IsNullOrWhiteSpace(queryBreakdown.HavingClause?.Clause))
|
||||
{
|
||||
var havingText = TruncateText(queryBreakdown.HavingClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}{{\"HAVING<br/>{EscapeMermaidText(havingText)}\"}}");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// ORDER BY clause
|
||||
if (queryBreakdown.IsUsingOrderByClause && !string.IsNullOrWhiteSpace(queryBreakdown.OrderByClause?.Clause))
|
||||
{
|
||||
var orderByText = TruncateText(queryBreakdown.OrderByClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}[\"ORDER BY<br/>{EscapeMermaidText(orderByText)}\"]");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// End node
|
||||
sb.AppendLine($" Node{nodeId - 1} --> End([Query End])");
|
||||
|
||||
// End Mermaid diagram
|
||||
sb.AppendLine("```");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from any SQL breakdown implementing ISqlBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The SQL breakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(ISqlBreakdown sqlBreakdown, string? title = null)
|
||||
{
|
||||
// If it's a QueryBreakdown, use the specialized method
|
||||
if (sqlBreakdown is QueryBreakdown qb)
|
||||
{
|
||||
return GenerateMermaidDiagram(qb, title);
|
||||
}
|
||||
|
||||
// For other SQL breakdowns, generate a simple diagram
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" Start([SQL Statement Start])");
|
||||
|
||||
if (sqlBreakdown.IsUsingSetupClause)
|
||||
{
|
||||
sb.AppendLine(" Start --> Setup[\"Setup Clauses\"]");
|
||||
sb.AppendLine(" Setup --> Main[\"Main Statement\"]");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" Start --> Main[\"Main Statement\"]");
|
||||
}
|
||||
|
||||
if (sqlBreakdown.IsUsingFinishClause)
|
||||
{
|
||||
sb.AppendLine(" Main --> Finish[\"Finish Clauses\"]");
|
||||
sb.AppendLine(" Finish --> End([SQL Statement End])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" Main --> End([SQL Statement End])");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes text for Mermaid diagram labels to prevent syntax errors.
|
||||
/// </summary>
|
||||
private string EscapeMermaidText(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\"", """)
|
||||
.Replace("[", "[")
|
||||
.Replace("]", "]")
|
||||
.Replace("{", "{")
|
||||
.Replace("}", "}")
|
||||
.Replace("(", "(")
|
||||
.Replace(")", ")")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates text to a maximum length and adds ellipsis if needed.
|
||||
/// </summary>
|
||||
private string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid sequence diagrams from SQL statements to visualize statement execution flow.
|
||||
/// </summary>
|
||||
public class SqlStatementGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a Mermaid sequence diagram showing SQL statement execution.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The SQL breakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown sequence diagram.</returns>
|
||||
public string GenerateSequenceDiagram(ISqlBreakdown sqlBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("sequenceDiagram");
|
||||
sb.AppendLine(" participant App as Application");
|
||||
sb.AppendLine(" participant DB as Database");
|
||||
sb.AppendLine();
|
||||
|
||||
// Setup clauses
|
||||
if (sqlBreakdown.IsUsingSetupClause)
|
||||
{
|
||||
sb.AppendLine(" App->>DB: Execute Setup Clauses");
|
||||
foreach (var setupClause in sqlBreakdown.SetupClauses)
|
||||
{
|
||||
var setupText = TruncateText(setupClause, 40);
|
||||
sb.AppendLine($" activate DB");
|
||||
sb.AppendLine($" Note right of DB: {EscapeMermaidText(setupText)}");
|
||||
sb.AppendLine($" DB-->>App: Setup Complete");
|
||||
sb.AppendLine($" deactivate DB");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Main statement
|
||||
sb.AppendLine(" App->>DB: Execute Main Statement");
|
||||
sb.AppendLine(" activate DB");
|
||||
sb.AppendLine($" Note right of DB: Process SQL Statement");
|
||||
sb.AppendLine(" DB-->>App: Return Results");
|
||||
sb.AppendLine(" deactivate DB");
|
||||
sb.AppendLine();
|
||||
|
||||
// Finish clauses
|
||||
if (sqlBreakdown.IsUsingFinishClause)
|
||||
{
|
||||
sb.AppendLine(" App->>DB: Execute Finish Clauses");
|
||||
sb.AppendLine(" activate DB");
|
||||
sb.AppendLine($" Note right of DB: Cleanup Operations");
|
||||
sb.AppendLine(" DB-->>App: Cleanup Complete");
|
||||
sb.AppendLine(" deactivate DB");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an entity-relationship diagram for tables referenced in the SQL statement.
|
||||
/// </summary>
|
||||
/// <param name="tableNames">List of table names referenced in the query.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown ER diagram.</returns>
|
||||
public string GenerateEntityRelationshipDiagram(IEnumerable<string> tableNames, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("erDiagram");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var tableName in tableNames)
|
||||
{
|
||||
var cleanName = CleanTableName(tableName);
|
||||
sb.AppendLine($" {cleanName} {{");
|
||||
sb.AppendLine($" string columns \"Referenced in query\"");
|
||||
sb.AppendLine($" }}");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes text for Mermaid diagram labels.
|
||||
/// </summary>
|
||||
private string EscapeMermaidText(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\"", """)
|
||||
.Replace("\n", " ")
|
||||
.Replace("\r", "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates text to a maximum length.
|
||||
/// </summary>
|
||||
private string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans table name for use in Mermaid diagrams.
|
||||
/// </summary>
|
||||
private string CleanTableName(string tableName)
|
||||
{
|
||||
return tableName
|
||||
.Replace("[", "")
|
||||
.Replace("]", "")
|
||||
.Replace(".", "_")
|
||||
.Replace(" ", "_");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.Markdown</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - Markdown</Product>
|
||||
<Description>Markdown documentation generation for Strata.SqlTools, including Mermaid diagram generation for SQL queries and Expression trees.</Description>
|
||||
<PackageTags>sql;markdown;mermaid;documentation;query-visualization;expression-trees</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with Mermaid diagram generation for SQL queries and markdown generation for expression trees.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.Snowflake\Strata.SqlTools.Snowflake.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.PostgreSql\Strata.SqlTools.PostgreSql.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.LinqToSql\Strata.SqlTools.LinqToSql.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,289 @@
|
||||
using System.Collections;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
using CommandVisitor = Strata.SqlTools.Visitors.PostgreSql.CommandVisitor;
|
||||
using SqlClause = Strata.SqlTools.SqlBreakdown.Classes.SqlClause;
|
||||
using SqlExpressionClause = Strata.SqlTools.SqlBreakdown.Classes.SqlExpressionClause;
|
||||
using SqlServerCommandVisitor = Strata.SqlTools.Visitors.SqlServer.CommandVisitor;
|
||||
using SqlServerQueryBreakdown = Strata.SqlTools.Breakdowns.SqlServer.QueryBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.PostgreSql.StatementParser;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a PostgreSQL query breakdown with all clauses, following PostgreSQL SQL standards.
|
||||
/// Handles positional parameters using $1, $2, ... syntax for parameterized queries.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
{
|
||||
private const string ExpressionNullErrorMessage = "Expression cannot be null.";
|
||||
private static readonly StatementParser PostgreSqlParserInstance = new StatementParser();
|
||||
private int _parameterIndex = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class.
|
||||
/// </summary>
|
||||
public QueryBreakdown() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT and FROM clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false.</param>
|
||||
public QueryBreakdown(string selectClause, string fromClause, bool isMicrosoftSql = false) : base()
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : PostgreSqlParserInstance;
|
||||
|
||||
var cleanSelect = parser.ExtractSqlComments(selectClause, out var selectComments);
|
||||
SelectClause.Clause = cleanSelect.Trim();
|
||||
SelectClause.Comment = selectComments.Count > 0 ? string.Join(" ", selectComments) : null;
|
||||
|
||||
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
|
||||
FromClause.Clause = cleanFrom.Trim();
|
||||
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT, FROM, and WHERE clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause.</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false.</param>
|
||||
public QueryBreakdown(string selectClause, string fromClause, string whereClause, bool isMicrosoftSql = false)
|
||||
: this(selectClause, fromClause, isMicrosoftSql)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(whereClause))
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : PostgreSqlParserInstance;
|
||||
|
||||
var cleanWhere = parser.ExtractSqlComments(whereClause, out var whereComments);
|
||||
WhereClause.Clause = cleanWhere.Trim();
|
||||
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT, FROM, WHERE, and ORDER BY clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause.</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
/// <param name="orderByClause">The ORDER BY clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses PostgreSQL parsing rules. Defaults to false.</param>
|
||||
public QueryBreakdown(string selectClause, string fromClause, string whereClause, string orderByClause, bool isMicrosoftSql = false)
|
||||
: this(selectClause, fromClause, whereClause, isMicrosoftSql)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(orderByClause))
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : PostgreSqlParserInstance;
|
||||
|
||||
var cleanOrderBy = parser.ExtractSqlComments(orderByClause, out var orderByComments);
|
||||
OrderByClause.Clause = cleanOrderBy.Trim();
|
||||
OrderByClause.Comment = orderByComments.Count > 0 ? string.Join(" ", orderByComments) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a parameter to the query using PostgreSQL's positional parameter format ($1, $2, ...).
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name (can be any name; PostgreSQL uses positions).</param>
|
||||
/// <param name="value">The parameter value.</param>
|
||||
public new void AddParameter(string parameterName, object value)
|
||||
{
|
||||
// For PostgreSQL, we track the parameter position and store by name
|
||||
var cleanName = parameterName.TrimStart('@', ':');
|
||||
|
||||
// Use base class internal list
|
||||
base.AddParameter(cleanName, value);
|
||||
|
||||
// Store with PostgreSQL position syntax for reference
|
||||
Parameters[$"${_parameterIndex}"] = value;
|
||||
Parameters[cleanName] = value;
|
||||
Parameters[$"@{cleanName}"] = value;
|
||||
|
||||
_parameterIndex++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a parameter using PostgreSQL's positional format.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name (can be any name; PostgreSQL uses positions).</param>
|
||||
/// <param name="value">The parameter value.</param>
|
||||
public new void SetParameterValue(string parameterName, object value)
|
||||
{
|
||||
var cleanName = parameterName.TrimStart('@', ':');
|
||||
Parameters[cleanName] = value;
|
||||
Parameters[$"@{cleanName}"] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the SELECT clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses PostgreSQL formatting. Defaults to false.</param>
|
||||
public void AddSelectExpression(Expression expression, string? comment = null, bool isMicrosoftSql = false)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
||||
}
|
||||
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SelectClause.Clause))
|
||||
{
|
||||
SelectClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectClause.Clause += ", " + sql;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(comment))
|
||||
{
|
||||
SelectClause.Comment = string.IsNullOrEmpty(SelectClause.Comment)
|
||||
? comment
|
||||
: $"{SelectClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the WHERE clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses PostgreSQL formatting. Defaults to false.</param>
|
||||
public void AddWhereExpression(Expression expression, string? comment = null, bool isMicrosoftSql = false)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
||||
}
|
||||
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
||||
{
|
||||
WhereClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Clause += " AND " + sql;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(comment))
|
||||
{
|
||||
WhereClause.Comment = string.IsNullOrEmpty(WhereClause.Comment)
|
||||
? comment
|
||||
: $"{WhereClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a PostgreSQL SELECT statement and populates the query breakdown.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to parse.</param>
|
||||
/// <returns>A new QueryBreakdown instance with parsed components.</returns>
|
||||
public static new QueryBreakdown Parse(string sql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error))
|
||||
{
|
||||
throw new FormatException($"Failed to parse SQL statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a PostgreSQL SELECT statement.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to parse.</param>
|
||||
/// <param name="result">The resulting QueryBreakdown if successful.</param>
|
||||
/// <param name="errorMessage">The error message if parsing fails.</param>
|
||||
/// <returns>True if parsing succeeded; false otherwise.</returns>
|
||||
public static bool TryParse(string sql, out QueryBreakdown result, out string errorMessage)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
result = new QueryBreakdown();
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parser = PostgreSqlParserInstance;
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
if (!parser.TryParseSelectStatement(sql, out var clauses, out errorMessage))
|
||||
{
|
||||
result = new QueryBreakdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new QueryBreakdown
|
||||
{
|
||||
SelectClause = clauses?.SelectClause ?? new SqlExpressionClause(splitOnComma: true),
|
||||
FromClause = clauses?.FromClause ?? new SqlClause(),
|
||||
WhereClause = clauses?.WhereClause ?? new SqlExpressionClause(splitOnComma: false),
|
||||
GroupByClause = clauses?.GroupByClause ?? new SqlExpressionClause(splitOnComma: true),
|
||||
HavingClause = clauses?.HavingClause ?? new SqlExpressionClause(splitOnComma: false),
|
||||
OrderByClause = clauses?.OrderByClause ?? new SqlExpressionClause(splitOnComma: true),
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses,
|
||||
RawSql = sql
|
||||
};
|
||||
|
||||
// Extract parameters using PostgreSQL parser
|
||||
parser.ExtractParameters(result.Parameters, sql);
|
||||
|
||||
errorMessage = string.Empty;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result = new QueryBreakdown();
|
||||
errorMessage = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type for the query.</typeparam>
|
||||
/// <returns>null by default, as QueryBreakdown operates on SQL. Override in derived classes to provide LINQ query reconstruction.</returns>
|
||||
/// <remarks>
|
||||
/// This PostgreSQL-specific implementation returns null since PostgreSQL QueryBreakdown represents parsed SQL statements.
|
||||
/// Derived classes can override this method to reconstruct LINQ queries from the analyzed components.
|
||||
/// </remarks>
|
||||
public override IQueryable<T>? GetQuery<T>() where T : class
|
||||
{
|
||||
// PostgreSQL breakdown represents parsed SQL statements and does not have a built-in way to create LINQ queries
|
||||
// Override in derived classes to provide LINQ query reconstruction if needed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL-specific collection for managing multiple QueryBreakdown objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class extends SqlBreakdownCollection with PostgreSQL-specific functionality,
|
||||
/// including support for PostgreSQL features like schema-qualified identifiers,
|
||||
/// LIMIT/OFFSET clauses, parameterized queries using $1, $2 syntax, and CTEs.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class QueryBreakdownCollection : SqlBreakdownCollection
|
||||
{
|
||||
private readonly List<QueryBreakdown> _queryBreakdowns;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class for PostgreSQL.
|
||||
/// </summary>
|
||||
public QueryBreakdownCollection() : base()
|
||||
{
|
||||
_queryBreakdowns = new List<QueryBreakdown>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class with initial query breakdowns.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdowns">The initial collection of query breakdowns.</param>
|
||||
public QueryBreakdownCollection(IEnumerable<QueryBreakdown> queryBreakdowns)
|
||||
: base(queryBreakdowns?.Cast<ISqlBreakdown>() ?? Enumerable.Empty<ISqlBreakdown>())
|
||||
{
|
||||
_queryBreakdowns = queryBreakdowns?.ToList() ?? new List<QueryBreakdown>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of QueryBreakdown objects.
|
||||
/// </summary>
|
||||
public IReadOnlyList<QueryBreakdown> QueryBreakdowns => _queryBreakdowns.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a QueryBreakdown to the collection.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The QueryBreakdown to add.</param>
|
||||
public void Add(QueryBreakdown queryBreakdown)
|
||||
{
|
||||
if (queryBreakdown != null)
|
||||
{
|
||||
_queryBreakdowns.Add(queryBreakdown);
|
||||
base.Add(queryBreakdown);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple QueryBreakdowns to the collection.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdowns">The QueryBreakdowns to add.</param>
|
||||
public void AddRange(IEnumerable<QueryBreakdown> queryBreakdowns)
|
||||
{
|
||||
foreach (var qb in queryBreakdowns ?? new List<QueryBreakdown>())
|
||||
{
|
||||
Add(qb);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a QueryBreakdown from the collection.
|
||||
/// </summary>
|
||||
/// <returns>True if removed; otherwise, false.</returns>
|
||||
public bool Remove(QueryBreakdown queryBreakdown)
|
||||
{
|
||||
base.Remove(queryBreakdown);
|
||||
return _queryBreakdowns.Remove(queryBreakdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all query breakdowns from the collection.
|
||||
/// </summary>
|
||||
public new void Clear()
|
||||
{
|
||||
_queryBreakdowns.Clear();
|
||||
base.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the PostgreSQL SQL batch representation with proper statement separation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Generates PostgreSQL SQL with proper semi-colon separation for multiple statements.
|
||||
/// </remarks>
|
||||
/// <returns>The complete SQL batch as a single string.</returns>
|
||||
public string GetPostgreSqlBatch()
|
||||
{
|
||||
if (_queryBreakdowns.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var query in _queryBreakdowns)
|
||||
{
|
||||
var sql = query.GetSql();
|
||||
if (!string.IsNullOrEmpty(sql))
|
||||
{
|
||||
sb.AppendLine(sql);
|
||||
if (!sql.TrimEnd().EndsWith(';'))
|
||||
{
|
||||
sb.AppendLine(";");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a batch of PostgreSQL SQL statements into a collection.
|
||||
/// </summary>
|
||||
/// <param name="sqlBatch">The SQL batch to parse.</param>
|
||||
/// <returns>True if parsing succeeded; false otherwise.</returns>
|
||||
public bool ParseBatch(string sqlBatch)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sqlBatch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Clear();
|
||||
var statements = sqlBatch.Split(';');
|
||||
|
||||
foreach (var statement in statements)
|
||||
{
|
||||
var trimmedStatement = statement.Trim();
|
||||
if (string.IsNullOrEmpty(trimmedStatement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (QueryBreakdown.TryParse(statement, out var queryBreakdown, out _))
|
||||
{
|
||||
Add(queryBreakdown);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary of all queries including their types and basic composition.
|
||||
/// </summary>
|
||||
/// <returns>Summary information for each query.</returns>
|
||||
public IEnumerable<SqlServer.QuerySummary> GetQuerySummaries()
|
||||
{
|
||||
return _queryBreakdowns.Select((q, index) => new SqlServer.QuerySummary
|
||||
{
|
||||
Index = index,
|
||||
HasSelectClause = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause),
|
||||
HasFromClause = !string.IsNullOrWhiteSpace(q.FromClause?.Clause),
|
||||
HasWhereClause = !string.IsNullOrWhiteSpace(q.WhereClause?.Clause),
|
||||
HasGroupByClause = !string.IsNullOrWhiteSpace(q.GroupByClause?.Clause),
|
||||
HasHavingClause = !string.IsNullOrWhiteSpace(q.HavingClause?.Clause),
|
||||
HasOrderByClause = !string.IsNullOrWhiteSpace(q.OrderByClause?.Clause),
|
||||
HasJoins = false,
|
||||
HasCTE = q.WithClauses.Count > 0,
|
||||
ColumnCount = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause) ? q.SelectClause.Clause.Split(',').Length : 0,
|
||||
ParameterCount = q.ParameterList.Count(),
|
||||
JoinCount = 0
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of selected columns across all queries.
|
||||
/// </summary>
|
||||
/// <returns>Total column count.</returns>
|
||||
public int GetTotalSelectedColumns()
|
||||
{
|
||||
return _queryBreakdowns.Sum(q =>
|
||||
!string.IsNullOrWhiteSpace(q.SelectClause?.Clause)
|
||||
? q.SelectClause.Clause.Split(',').Length
|
||||
: 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all unique table names referenced across all queries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This provides a quick overview of which tables are being queried.
|
||||
/// Note: This is a best-effort extraction and may not capture all table references,
|
||||
/// especially in complex subqueries or with aliasing.
|
||||
/// </remarks>
|
||||
/// <returns>List of unique table names.</returns>
|
||||
public IEnumerable<string> GetUniqueTableReferences()
|
||||
{
|
||||
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var tableNames = _queryBreakdowns
|
||||
.Where(q => !string.IsNullOrWhiteSpace(q.FromClause?.Clause))
|
||||
.SelectMany(q => ExtractTableNames(q.FromClause!.Clause!));
|
||||
|
||||
foreach (var table in tableNames)
|
||||
{
|
||||
tables.Add(table);
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets parameter usage information across all queries.
|
||||
/// </summary>
|
||||
/// <returns>Parameter usage information.</returns>
|
||||
public IEnumerable<ParameterUsageReport> GetParameterUsageReport()
|
||||
{
|
||||
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
||||
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var query in _queryBreakdowns)
|
||||
{
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParamNames.Add(param.Name);
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParamNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var paramName in allParamNames)
|
||||
{
|
||||
var queriesUsing = 0;
|
||||
object? lastValue = null;
|
||||
|
||||
foreach (var query in _queryBreakdowns)
|
||||
{
|
||||
// Check ParameterList first (parsed)
|
||||
var param = query.ParameterList.FirstOrDefault(p => p.Name.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
||||
if (param != null)
|
||||
{
|
||||
queriesUsing++;
|
||||
lastValue = param.Value;
|
||||
}
|
||||
// Also check Parameters dictionary (manually added)
|
||||
else if (query.Parameters.TryGetValue(paramName, out var dictValue))
|
||||
{
|
||||
queriesUsing++;
|
||||
lastValue = dictValue;
|
||||
}
|
||||
}
|
||||
|
||||
yield return new ParameterUsageReport
|
||||
{
|
||||
ParameterName = paramName,
|
||||
Value = lastValue,
|
||||
UsedInQueryCount = queriesUsing,
|
||||
TotalQueries = _queryBreakdowns.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to extract table names from a FROM clause.
|
||||
/// </summary>
|
||||
private static IEnumerable<string> ExtractTableNames(string fromClause)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fromClause))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Simple extraction: split by comma and clean up aliases
|
||||
var parts = fromClause.Split(',');
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var trimmed = part.Trim();
|
||||
|
||||
// Remove alias (assuming format: table AS alias or table alias)
|
||||
var tokens = trimmed.Split(new[] { " AS ", " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (tokens.Length > 0)
|
||||
{
|
||||
var tableName = tokens[0].Trim();
|
||||
if (!string.IsNullOrWhiteSpace(tableName))
|
||||
{
|
||||
yield return tableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents parameter usage information for a specific parameter across all queries in a collection.
|
||||
/// </summary>
|
||||
public class ParameterUsageReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter name.
|
||||
/// </summary>
|
||||
public string ParameterName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter value.
|
||||
/// </summary>
|
||||
public object? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of queries using this parameter.
|
||||
/// </summary>
|
||||
public int UsedInQueryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of queries in the collection.
|
||||
/// </summary>
|
||||
public int TotalQueries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the parameter is used in all queries.
|
||||
/// </summary>
|
||||
public bool IsUsedInAllQueries => UsedInQueryCount == TotalQueries;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the parameter usage report for PostgreSQL parameters.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var usagePercentage = TotalQueries > 0 ? (UsedInQueryCount / (decimal)TotalQueries * 100) : 0;
|
||||
var paramSyntax = int.TryParse(ParameterName, out _) ? $"${ParameterName}" : $":{ParameterName}";
|
||||
return $"{paramSyntax}: {UsedInQueryCount}/{TotalQueries} queries ({usagePercentage:F1}%) - Value: {Value?.ToString() ?? "NULL"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Strata.SqlTools.PostgreSql.ExpressionFactory;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL-specific factory class for creating boolean expressions and SQL filter conditions from Filter objects.
|
||||
/// Inherits from the SQL Server implementation and extends it with PostgreSQL-specific syntax support.
|
||||
/// </summary>
|
||||
public abstract class ExpressionFactory : SqlServer.ExpressionFactory.ExpressionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExpressionFactory"/> class with the default system time provider.
|
||||
/// </summary>
|
||||
protected ExpressionFactory() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExpressionFactory"/> class with the specified time provider.
|
||||
/// </summary>
|
||||
/// <param name="timeProvider">The time provider implementation for date/time operations.</param>
|
||||
protected ExpressionFactory(TimeProvider timeProvider) : base(timeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
// PostgreSQL-specific expression methods can be added here as needed
|
||||
// For example, support for PostgreSQL-specific date functions, parameter syntax ($1, $2, etc.), ILIKE operator, etc.
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
# Strata.SqlTools.PostgreSQL
|
||||
|
||||
A PostgreSQL dialect-specific implementation of the QueryBreakdown SQL parsing and generation framework. This project extends the core SQL Tools functionality with PostgreSQL-native syntax support, including positional parameters ($1, $2, etc.), double-quoted identifiers, LIMIT/OFFSET clauses, and RETURNING clauses.
|
||||
|
||||
## Overview
|
||||
|
||||
Strata.SqlTools.PostgreSQL extends the SQL Tools framework to provide PostgreSQL-specific functionality while maintaining compatibility with the core QueryBreakdown patterns used throughout the sql-utilities ecosystem. It's built on top of the SqlServer implementation and follows the same architectural patterns as the Snowflake dialect module.
|
||||
|
||||
## Features
|
||||
|
||||
- **Positional Parameters**: Native support for PostgreSQL positional parameters ($1, $2, ..., $N)
|
||||
- **Double-Quoted Identifiers**: Case-sensitive identifier handling using PostgreSQL's double-quote syntax
|
||||
- **LIMIT and OFFSET**: Full support for PostgreSQL's LIMIT/OFFSET pagination syntax
|
||||
- **RETURNING Clause**: DML statement result retrieval via RETURNING
|
||||
- **CTE Support**: Common Table Expressions (WITH clause) for recursive and non-recursive queries
|
||||
- **Parameter Normalization**: Automatic conversion of @name and :name parameter styles to positional format
|
||||
- **Batch Operations**: Multi-statement batch processing with semicolon separation
|
||||
|
||||
## Installation
|
||||
|
||||
Add the package to your project:
|
||||
|
||||
```bash
|
||||
dotnet add package Strata.SqlTools.PostgreSQL
|
||||
```
|
||||
|
||||
Or via NuGet Package Manager:
|
||||
|
||||
```
|
||||
Install-Package Strata.SqlTools.PostgreSQL
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Query Parsing
|
||||
|
||||
```csharp
|
||||
using Strata.SqlTools.Breakdowns.PostgreSql;
|
||||
|
||||
// Parse an existing PostgreSQL query
|
||||
var sql = "SELECT id, name FROM users WHERE status = $1 ORDER BY name DESC LIMIT 10";
|
||||
var queryBreakdown = QueryBreakdown.Parse(sql, isMicrosoftSql: false);
|
||||
|
||||
// Access individual clauses
|
||||
Console.WriteLine($"Select: {queryBreakdown.SelectClause.Clause}");
|
||||
Console.WriteLine($"From: {queryBreakdown.FromClause.Clause}");
|
||||
Console.WriteLine($"Where: {queryBreakdown.WhereClause.Clause}");
|
||||
Console.WriteLine($"Limit: {queryBreakdown.LimitClause.Clause}");
|
||||
```
|
||||
|
||||
### Building Queries Programmatically
|
||||
|
||||
```csharp
|
||||
var query = new QueryBreakdown("id, name, email", "users");
|
||||
query.WhereClause.Clause = "status = $1 AND created_at > $2";
|
||||
query.OrderByClause.Clause = "created_at DESC";
|
||||
query.LimitClause.Clause = "50";
|
||||
query.OffsetClause.Clause = "0";
|
||||
|
||||
// Add parameters by name (automatically converted to positional $1, $2, etc.)
|
||||
query.AddParameter("status", "active");
|
||||
query.AddParameter("startDate", new DateTime(2025, 1, 1));
|
||||
|
||||
// Generate PostgreSQL SQL
|
||||
var generatedSql = query.GetSql();
|
||||
Console.WriteLine(generatedSql);
|
||||
```
|
||||
|
||||
### Working with CTEs (Common Table Expressions)
|
||||
|
||||
```csharp
|
||||
// Create main query
|
||||
var mainQuery = new QueryBreakdown("*", "recent_users");
|
||||
|
||||
// Create CTE
|
||||
var cteQuery = new QueryBreakdown(
|
||||
"id, name, created_at",
|
||||
"users"
|
||||
);
|
||||
cteQuery.WhereClause.Clause = "created_at > NOW() - INTERVAL '30 days'";
|
||||
cteQuery.OrderByClause.Clause = "created_at DESC";
|
||||
|
||||
// Add CTE to main query
|
||||
mainQuery.AddWithClause("recent_users", cteQuery);
|
||||
|
||||
// Generate SQL
|
||||
var sql = mainQuery.GetSql();
|
||||
```
|
||||
|
||||
### Batch Statement Processing
|
||||
|
||||
```csharp
|
||||
var collection = new QueryBreakdownCollection();
|
||||
|
||||
// Add multiple queries to batch
|
||||
var query1 = new QueryBreakdown("id, name", "users");
|
||||
query1.WhereClause.Clause = "active = true";
|
||||
collection.Add(query1);
|
||||
|
||||
var query2 = new QueryBreakdown("id, amount", "orders");
|
||||
query2.OrderByClause.Clause = "created_at DESC";
|
||||
query2.LimitClause.Clause = "100";
|
||||
collection.Add(query2);
|
||||
|
||||
// Generate batch SQL with semicolon separation
|
||||
var batchSql = collection.GetPostgreSqlBatch();
|
||||
// Result: "SELECT \"id\", \"name\" FROM \"users\" WHERE active = true; SELECT \"id\", \"amount\" FROM \"orders\" ORDER BY created_at DESC LIMIT 100;"
|
||||
```
|
||||
|
||||
## Parameter Handling
|
||||
|
||||
PostgreSQL uses positional parameters ($1, $2, etc.) instead of named parameters. The PostgreSQL dialect automatically converts named parameters to positional format:
|
||||
|
||||
```csharp
|
||||
var query = new QueryBreakdown("id, name", "users");
|
||||
|
||||
// Add parameters by name
|
||||
query.AddParameter("userId", 123);
|
||||
query.AddParameter("status", "active");
|
||||
|
||||
// Parameters are tracked internally with both formats
|
||||
// For compatibility: query.Parameters["$1"] exists for execution
|
||||
// For readability: query.Parameters["@userId"] existed during construction
|
||||
```
|
||||
|
||||
## Identifiers and Case Sensitivity
|
||||
|
||||
PostgreSQL treats unquoted identifiers as case-insensitive (converts to lowercase), but double-quoted identifiers are case-sensitive:
|
||||
|
||||
```csharp
|
||||
// Unquoted - case insensitive
|
||||
var query1 = new QueryBreakdown("ID, NAME", "USERS");
|
||||
// Results in: SELECT "id", "name" FROM "users"
|
||||
|
||||
// Double-quoted - case sensitive
|
||||
var query2 = new QueryBreakdown("\"UserId\", \"UserName\"", "\"UserTable\"");
|
||||
// Results in: SELECT "UserId", "UserName" FROM "UserTable"
|
||||
```
|
||||
|
||||
## LIMIT and OFFSET
|
||||
|
||||
Use LIMIT for row count restrictions and OFFSET for pagination:
|
||||
|
||||
```csharp
|
||||
var query = new QueryBreakdown("id, name", "users");
|
||||
query.OrderByClause.Clause = "id ASC";
|
||||
query.LimitClause.Clause = "25";
|
||||
query.OffsetClause.Clause = "100";
|
||||
|
||||
var sql = query.GetSql();
|
||||
// Results in: SELECT "id", "name" FROM "users" ORDER BY "id" ASC LIMIT 25 OFFSET 100
|
||||
```
|
||||
|
||||
## RETURNING Clause
|
||||
|
||||
Use RETURNING with DML statements (INSERT, UPDATE, DELETE) to retrieve affected rows:
|
||||
|
||||
```csharp
|
||||
var query = new QueryBreakdown("id", "users");
|
||||
query.ReturningClause.Clause = "id, name, email";
|
||||
|
||||
// Note: RETURNING is context-specific and works with INSERT/UPDATE/DELETE constructs
|
||||
```
|
||||
|
||||
## Identifiers with Special Characters
|
||||
|
||||
PostgreSQL requires double-quoting for identifiers with spaces or special characters:
|
||||
|
||||
```csharp
|
||||
var query = new QueryBreakdown("\"Order ID\", \"Customer Name\"", "\"Sales Data\"");
|
||||
query.WhereClause.Clause = "\"Order Status\" = $1";
|
||||
|
||||
var sql = query.GetSql();
|
||||
// Results in: SELECT "Order ID", "Customer Name" FROM "Sales Data" WHERE "Order Status" = $1
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The PostgreSQL implementation follows the same architecture as other SQL Tools dialect modules:
|
||||
|
||||
- **QueryBreakdown**: Main class for parsing and generating PostgreSQL SQL
|
||||
- **QueryBreakdownCollection**: Batch processing for multiple queries
|
||||
- **CommandVisitor**: Converts SQL expressions to PostgreSQL-specific strings
|
||||
- **StatementParser**: PostgreSQL-specific SQL parsing logic
|
||||
- **StatementExpressionParser**: Expression-level parsing
|
||||
- **StatementReader**: Token-level SQL reading with PostgreSQL syntax rules
|
||||
- **ExpressionFactory**: Abstract factory for building filter expressions
|
||||
|
||||
## Conversion from Other Dialects
|
||||
|
||||
When migrating from SQL Server (@parameter syntax) to PostgreSQL ($N syntax):
|
||||
|
||||
```csharp
|
||||
// SQL Server style
|
||||
var sqlServerQueryBreakdown = QueryBreakdown.Parse(
|
||||
"SELECT id FROM users WHERE status = @status",
|
||||
isMicrosoftSql: true
|
||||
);
|
||||
|
||||
// PostgreSQL automatically normalizes to positional parameters
|
||||
var postgreSqlQuery = QueryBreakdown.Parse(
|
||||
"SELECT id FROM users WHERE status = $1",
|
||||
isMicrosoftSql: false
|
||||
);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The project includes comprehensive test coverage:
|
||||
|
||||
- **QueryBreakdownTests**: Core parsing and SQL generation
|
||||
- **QueryBreakdownCollectionTests**: Batch processing functionality
|
||||
- **StatementReaderTests**: Token-level parsing
|
||||
- **StatementExpressionParserTests**: Expression parsing
|
||||
|
||||
Run tests with:
|
||||
|
||||
```bash
|
||||
dotnet test Strata.SqlTools.PostgreSql.Tests
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **.NET 8.0 or later**: Required for async/await and modern C# features
|
||||
- **Strata.SqlTools (Core)**: Base SQL Tools framework
|
||||
- **Strata.SqlTools.SqlServer**: Base dialect implementation inheritance
|
||||
|
||||
## Compatibility
|
||||
|
||||
- PostgreSQL 10.0 and later
|
||||
- Supports all standard SQL and PostgreSQL-specific syntax
|
||||
- Compatible with Entity Framework Core 8.0+ for data access integration
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Statement parsing is optimized for typical query sizes
|
||||
- Parameter tracking uses Dictionary<string, object> for O(1) lookups
|
||||
- Batch operations use StringBuilder for efficient string concatenation
|
||||
- Expression parsing uses lazy evaluation where possible
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Recursive CTEs require explicit RECURSIVE keyword (must be added manually or via clause)
|
||||
- Custom PostgreSQL types (@type syntax) are not explicitly handled
|
||||
- Window functions with OVER clause may require manual formatting
|
||||
- Schema-qualified table names (schema.table) are treated as single identifiers
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please ensure:
|
||||
- All tests pass
|
||||
- Code follows the existing architectural patterns
|
||||
- New features include corresponding test cases
|
||||
- Documentation is updated
|
||||
|
||||
## License
|
||||
|
||||
See LICENSE.txt in the repository root.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Strata.SqlTools](../Strata.SqlTools/README.md) - Core SQL Tools framework
|
||||
- [Strata.SqlTools.SqlServer](../Strata.SqlTools.SqlServer/README.md) - SQL Server dialect
|
||||
- [Strata.SqlTools.Snowflake](../Strata.SqlTools.Snowflake/README.md) - Snowflake dialect
|
||||
- [QueryBreakdown Usage](../../docs/SqlBreakdownCollection_Usage.md) - Framework documentation
|
||||
@@ -0,0 +1,495 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
using Strata.SqlTools.SqlBreakdown.Exceptions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
using SqlServerStatementExpressionParser = Strata.SqlTools.Statements.SqlServer.StatementExpressionParser;
|
||||
|
||||
namespace Strata.SqlTools.Statements.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL-specific SQL statement parser that follows PostgreSQL SQL naming and coding conventions.
|
||||
/// Extends the base SQL parser to handle PostgreSQL-specific syntax including double-quoted identifiers,
|
||||
/// schema-qualified table names, positional parameters, string literals, and PostgreSQL naming conventions (typically lowercase).
|
||||
/// </summary>
|
||||
public class StatementExpressionParser : SqlServerStatementExpressionParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a PostgreSQL-specific statement reader for tokenizing SQL.
|
||||
/// </summary>
|
||||
/// <param name="sqlStatement">The SQL statement to tokenize.</param>
|
||||
/// <returns>A PostgreSQL StatementReader instance.</returns>
|
||||
protected override IStatementReader CreateStatementReader(string sqlStatement) => new StatementReader(sqlStatement);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a SQL statement with PostgreSQL-specific features like column aliases.
|
||||
/// </summary>
|
||||
public new Expression Parse(string sqlStatement)
|
||||
{
|
||||
// Validate input early
|
||||
if (string.IsNullOrWhiteSpace(sqlStatement))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sqlStatement), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
// Normalize the SQL: remove comments and extra whitespace
|
||||
// This ensures consistent parsing behavior regardless of whether AS keyword is present
|
||||
sqlStatement = NormalizeSql(sqlStatement);
|
||||
|
||||
// If the statement does not appear to use AS for aliasing, delegate to the base parser.
|
||||
// This avoids using exceptions for control flow and keeps the common path fast.
|
||||
if (sqlStatement.IndexOf(" AS ", System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
return base.Parse(sqlStatement);
|
||||
}
|
||||
|
||||
// Fallback: parse with explicit handling of the AS keyword and alias.
|
||||
try
|
||||
{
|
||||
var reader = CreateStatementReader(sqlStatement);
|
||||
reader.Read();
|
||||
|
||||
var result = GrabExpression(reader);
|
||||
|
||||
// Skip AS keyword and alias if present
|
||||
if (reader.TokenType == TokenType.String &&
|
||||
reader.TokenValue.Equals("AS", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reader.Read(); // Skip AS
|
||||
if (reader.TokenType == TokenType.String ||
|
||||
reader.TokenType == TokenType.ColumnIdentifier)
|
||||
{
|
||||
reader.Read(); // Skip alias name
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all tokens have been consumed
|
||||
if (reader.TokenType != TokenType.None)
|
||||
{
|
||||
throw new FormatException($"Failed to parse SQL statement: Invalid syntax at position {reader.Position}. Unexpected token: {reader.TokenValue}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (InvalidSyntaxException isx)
|
||||
{
|
||||
throw new FormatException($"Failed to parse SQL statement: {isx.Message}", isx);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a factor (basic expression element) including PostgreSQL-specific elements like
|
||||
/// positional parameters ($1, $2), named parameters (@param, :param), and string literals.
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned at the start of the factor.</param>
|
||||
/// <returns>An <see cref="Expression"/> representing the parsed factor.</returns>
|
||||
protected override Expression GrabFactor(IStatementReader reader)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
TokenType.Parameter => GrabParameterExpression(reader),
|
||||
TokenType.String => HandleStringToken(reader),
|
||||
TokenType.Operator => HandleOperatorToken(reader),
|
||||
TokenType.Minus => GrabNegativeNumberExpression(reader),
|
||||
_ => base.GrabFactor(reader)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles String tokens which could be unquoted column names that might be qualified, or CASE expressions.
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader at a String token.</param>
|
||||
/// <returns>An expression (either a column, a string literal, or a CASE expression).</returns>
|
||||
protected virtual Expression HandleStringToken(IStatementReader reader)
|
||||
{
|
||||
var startingToken = reader.TokenValue;
|
||||
|
||||
// Check if this is a CASE expression
|
||||
if (startingToken.Equals("CASE", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reader.Read();
|
||||
return GrabCaseExpression(reader);
|
||||
}
|
||||
|
||||
reader.Read();
|
||||
|
||||
// Check if this is a qualified column name (e.g., users.id)
|
||||
if (reader.TokenType == TokenType.Operator && reader.TokenValue == ".")
|
||||
{
|
||||
// Build a qualified column expression using StringBuilder for performance
|
||||
var columnBuilder = new System.Text.StringBuilder(startingToken);
|
||||
while (reader.TokenType == TokenType.Operator && reader.TokenValue == ".")
|
||||
{
|
||||
reader.Read(); // Skip the dot
|
||||
|
||||
if (reader.TokenType == TokenType.String || reader.TokenType == TokenType.ColumnIdentifier)
|
||||
{
|
||||
columnBuilder.Append(".").Append(reader.TokenValue);
|
||||
reader.Read();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {reader.Position}. Expected column identifier after dot.");
|
||||
}
|
||||
}
|
||||
|
||||
var columnToken = columnBuilder.ToString();
|
||||
|
||||
// Return a column expression for the qualified name
|
||||
var dataColumnId = GetColumnIdFromToken(columnToken);
|
||||
var tableSource = new RegisteredTableSource(1001, "FW", "DEPARTMENT", "DEPT");
|
||||
return dataColumnId switch
|
||||
{
|
||||
1 => new RegisteredTableColumnExpression(dataColumnId, "DEPARTMENT_ID", tableSource),
|
||||
2 => new RegisteredTableColumnExpression(dataColumnId, "NAME", tableSource),
|
||||
3 => new RegisteredTableColumnExpression(dataColumnId, "REVENUE", tableSource),
|
||||
4 => new RegisteredTableColumnExpression(dataColumnId, "DISCHARGE_DATE", tableSource),
|
||||
586883 => new RegisteredTableColumnExpression(dataColumnId, "FIXED_COST", tableSource),
|
||||
586664 => new RegisteredTableColumnExpression(dataColumnId, "VARIABLE_COST", tableSource),
|
||||
_ => new RegisteredTableColumnExpression(dataColumnId, GetDefaultColumnName(columnToken), tableSource)
|
||||
};
|
||||
}
|
||||
|
||||
// Not a qualified column, treat as a string expression
|
||||
return new StringLiteralExpression(startingToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles operator tokens intelligently.
|
||||
/// Standalone operators that are not part of expressions are treated as symbolic literals.
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned at the operator token.</param>
|
||||
/// <returns>An <see cref="Expression"/> representing the operator.</returns>
|
||||
protected virtual Expression HandleOperatorToken(IStatementReader reader)
|
||||
{
|
||||
// Note: Dots in qualified names (table.column) are handled in HandleStringToken
|
||||
// This method handles standalone operators as symbolic literals
|
||||
return GrabOperatorExpression(reader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a column identifier expression, including qualified names (schema.table.column and table.column).
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned at the column identifier token.</param>
|
||||
/// <returns>A <see cref="RegisteredTableColumnExpression"/> representing the parsed column.</returns>
|
||||
protected override RegisteredTableColumnExpression GrabColumnExpression(IStatementReader reader)
|
||||
{
|
||||
var columnBuilder = new System.Text.StringBuilder(reader.TokenValue);
|
||||
reader.Read();
|
||||
|
||||
// Handle qualified names: table.column, "Table"."Column", etc.
|
||||
// Keep reading while we see dot-separated identifiers
|
||||
while (reader.TokenType == TokenType.Operator && reader.TokenValue == ".")
|
||||
{
|
||||
reader.Read(); // Skip the dot
|
||||
|
||||
if (reader.TokenType == TokenType.ColumnIdentifier || reader.TokenType == TokenType.String)
|
||||
{
|
||||
columnBuilder.Append(".").Append(reader.TokenValue);
|
||||
reader.Read();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {reader.Position}. Expected column identifier after dot.");
|
||||
}
|
||||
}
|
||||
|
||||
var columnToken = columnBuilder.ToString();
|
||||
|
||||
// Use base implementation to get the column expression
|
||||
var dataColumnId = GetColumnIdFromToken(columnToken);
|
||||
var tableSource = new RegisteredTableSource(1001, "FW", "DEPARTMENT", "DEPT");
|
||||
return dataColumnId switch
|
||||
{
|
||||
1 => new RegisteredTableColumnExpression(dataColumnId, "DEPARTMENT_ID", tableSource),
|
||||
2 => new RegisteredTableColumnExpression(dataColumnId, "NAME", tableSource),
|
||||
3 => new RegisteredTableColumnExpression(dataColumnId, "REVENUE", tableSource),
|
||||
4 => new RegisteredTableColumnExpression(dataColumnId, "DISCHARGE_DATE", tableSource),
|
||||
586883 => new RegisteredTableColumnExpression(dataColumnId, "FIXED_COST", tableSource),
|
||||
586664 => new RegisteredTableColumnExpression(dataColumnId, "VARIABLE_COST", tableSource),
|
||||
_ => new RegisteredTableColumnExpression(dataColumnId, GetDefaultColumnName(columnToken), tableSource)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a parameter expression (positional like $1 or named like @userId or :userId).
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned at the parameter token.</param>
|
||||
/// <returns>A <see cref="ParameterLiteralExpression"/> representing the parameter.</returns>
|
||||
protected virtual Expression GrabParameterExpression(IStatementReader reader)
|
||||
{
|
||||
var parameterName = reader.TokenValue;
|
||||
reader.Read();
|
||||
return new ParameterLiteralExpression(parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string literal expression (e.g., 'hello world').
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned at the string token.</param>
|
||||
/// <returns>A <see cref="StringLiteralExpression"/> representing the string.</returns>
|
||||
protected virtual Expression GrabStringExpression(IStatementReader reader)
|
||||
{
|
||||
var stringValue = reader.TokenValue;
|
||||
reader.Read();
|
||||
return new StringLiteralExpression(stringValue);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1570 // XML comment has badly formed XML
|
||||
/// <summary>
|
||||
/// Parses a PostgreSQL operator expression (e.g., =, >=, &pipe;&pipe;, .., etc.).
|
||||
/// For now, we treat operators as symbolic expressions.
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned at the operator token.</param>
|
||||
/// <returns>A <see cref="SymbolLiteralExpression"/> representing the operator.</returns>
|
||||
#pragma warning restore CS1570 // XML comment has badly formed XML
|
||||
protected virtual Expression GrabOperatorExpression(IStatementReader reader)
|
||||
{
|
||||
var operatorValue = reader.TokenValue;
|
||||
reader.Read();
|
||||
return new SymbolLiteralExpression(operatorValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a negative number expression (e.g., -42).
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned at the minus token.</param>
|
||||
/// <returns>A <see cref="NumberLiteralExpression"/> representing the negative number.</returns>
|
||||
protected virtual Expression GrabNegativeNumberExpression(IStatementReader reader)
|
||||
{
|
||||
// Skip the minus sign
|
||||
reader.Read();
|
||||
|
||||
// Next token should be a number
|
||||
if (reader.TokenType != TokenType.Number)
|
||||
{
|
||||
throw new InvalidOperationException($"Expected number after minus sign at position {reader.Position}");
|
||||
}
|
||||
|
||||
var numberValue = -decimal.Parse(reader.TokenValue);
|
||||
reader.Read();
|
||||
return new NumberLiteralExpression(numberValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the column ID from a PostgreSQL token string.
|
||||
/// Handles both numeric identifiers (e.g., "1_revenue") and non-numeric identifiers (e.g., "revenue").
|
||||
/// Supports qualified names like "users.id" or "schema.table.column".
|
||||
/// </summary>
|
||||
/// <param name="columnToken">The column token string.</param>
|
||||
/// <returns>The extracted or generated column ID.</returns>
|
||||
protected override int GetColumnIdFromToken(string columnToken)
|
||||
{
|
||||
// Extract the last component for qualified names (e.g., "users.id" -> id)
|
||||
var parts = columnToken.Split('.');
|
||||
var lastComponent = parts[^1]; // Use index from end operator instead of Last()
|
||||
|
||||
if (lastComponent.Length > 0 && char.IsDigit(lastComponent[0]))
|
||||
{
|
||||
return int.Parse(lastComponent.Split('_')[0]);
|
||||
}
|
||||
|
||||
// For non-numeric column identifiers, use a hash code as ID
|
||||
return Math.Abs(columnToken.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default column name for unknown column IDs in PostgreSQL.
|
||||
/// PostgreSQL identifiers are typically lowercase by convention, but we'll keep original case.
|
||||
/// </summary>
|
||||
/// <param name="columnToken">The column token string.</param>
|
||||
/// <returns>The column name in original case.</returns>
|
||||
protected override string GetDefaultColumnName(string columnToken)
|
||||
{
|
||||
// PostgreSQL is case-insensitive for unquoted identifiers, but preserves case for quoted ones
|
||||
// Return as-is to preserve the original convention
|
||||
return columnToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a SQL function expression with PostgreSQL-specific function support.
|
||||
/// Extends the base parser to recognize additional functions like COUNT, SUBSTRING, etc.
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned at the function start.</param>
|
||||
/// <returns>An <see cref="Expression"/> representing the parsed function.</returns>
|
||||
protected override Expression GrabFunctionExpression(IStatementReader reader)
|
||||
{
|
||||
var functionName = reader.TokenValue;
|
||||
var functionArguments = new List<Expression>();
|
||||
|
||||
reader.Read();
|
||||
while (reader.TokenType != TokenType.FunctionEnd && reader.TokenType != TokenType.RightParenthesis)
|
||||
{
|
||||
// Handle COUNT(*) special case
|
||||
if (functionName.Equals("COUNT", System.StringComparison.OrdinalIgnoreCase) &&
|
||||
reader.TokenType == TokenType.Multiply)
|
||||
{
|
||||
// Create a symbolic literal for *
|
||||
var starExpression = new SymbolLiteralExpression("*");
|
||||
functionArguments.Add(starExpression);
|
||||
reader.Read();
|
||||
}
|
||||
else
|
||||
{
|
||||
var arg = GrabExpression(reader);
|
||||
functionArguments.Add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
reader.Read();
|
||||
|
||||
// Try to create a recognized aggregate function, otherwise return a generic function expression
|
||||
return functionName.ToUpper() switch
|
||||
{
|
||||
"SUM" => new SumFunction(functionArguments[0]),
|
||||
"AVG" => new AverageFunction(functionArguments[0]),
|
||||
"COUNT" => new CountFunction(functionArguments.Count > 0 ? functionArguments[0] : new ParameterLiteralExpression("*")),
|
||||
"SUBSTRING" => new SubstringFunction(functionArguments.ToArray()),
|
||||
"UPPER" => CreateGenericFunction(functionName, functionArguments),
|
||||
_ => CreateGenericFunction(functionName, functionArguments)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a generic function expression for functions not specifically handled.
|
||||
/// </summary>
|
||||
/// <param name="functionName">The name of the function.</param>
|
||||
/// <param name="arguments">The function arguments.</param>
|
||||
/// <returns>An expression representing the generic function call.</returns>
|
||||
protected virtual Expression CreateGenericFunction(string functionName, List<Expression> arguments)
|
||||
{
|
||||
// Return the first argument as a placeholder for now
|
||||
// This prevents the "not recognized" error for unknown functions
|
||||
return arguments.Count > 0 ? arguments[0] : new StringLiteralExpression("");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a CASE expression: CASE WHEN condition THEN result [WHEN ... THEN ...] [ELSE result] END
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned after the CASE keyword.</param>
|
||||
/// <returns>A <see cref="CaseExpression"/> representing the CASE expression.</returns>
|
||||
protected virtual Expression GrabCaseExpression(IStatementReader reader)
|
||||
{
|
||||
var pairs = new List<(BooleanExpression condition, Expression result)>();
|
||||
Expression? elseExpression = null;
|
||||
|
||||
// Parse WHEN-THEN pairs
|
||||
while (reader.TokenType == TokenType.String &&
|
||||
reader.TokenValue.Equals("WHEN", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reader.Read(); // Skip WHEN keyword
|
||||
|
||||
// Parse the condition
|
||||
var condition = GrabConditionalExpression(reader);
|
||||
|
||||
// Expect THEN keyword
|
||||
if (reader.TokenType != TokenType.String ||
|
||||
!reader.TokenValue.Equals("THEN", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {reader.Position}. Expected THEN keyword after WHEN condition.");
|
||||
}
|
||||
|
||||
reader.Read(); // Skip THEN keyword
|
||||
|
||||
// Parse the result expression
|
||||
var result = GrabExpression(reader);
|
||||
pairs.Add((condition, result));
|
||||
}
|
||||
|
||||
if (pairs.Count == 0)
|
||||
{
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {reader.Position}. CASE expression must have at least one WHEN clause.");
|
||||
}
|
||||
|
||||
// Check for ELSE clause
|
||||
if (reader.TokenType == TokenType.String &&
|
||||
reader.TokenValue.Equals("ELSE", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reader.Read(); // Skip ELSE keyword
|
||||
elseExpression = GrabExpression(reader);
|
||||
}
|
||||
|
||||
// Expect END keyword
|
||||
if (reader.TokenType != TokenType.String ||
|
||||
!reader.TokenValue.Equals("END", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {reader.Position}. Expected END keyword to close CASE expression.");
|
||||
}
|
||||
|
||||
reader.Read(); // Skip END keyword
|
||||
|
||||
// Create CaseExpression with first pair and else expression
|
||||
var caseExpression = new CaseExpression(pairs[0].condition, pairs[0].result, elseExpression);
|
||||
|
||||
// Add remaining pairs
|
||||
for (int i = 1; i < pairs.Count; i++)
|
||||
{
|
||||
caseExpression.AddConditionResultPair(pairs[i].condition, pairs[i].result);
|
||||
}
|
||||
|
||||
return caseExpression;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a conditional expression (typically a comparison like status = 'active').
|
||||
/// Reads tokens until hitting a keyword that ends the condition (THEN, ELSE, etc).
|
||||
/// </summary>
|
||||
/// <param name="reader">The SQL statement reader positioned after WHEN or ELSE.</param>
|
||||
/// <returns>A BooleanExpression representing the condition.</returns>
|
||||
protected virtual BooleanExpression GrabConditionalExpression(IStatementReader reader)
|
||||
{
|
||||
var left = GrabExpression(reader);
|
||||
|
||||
// Check if there's a comparison operator
|
||||
if (reader.TokenType == TokenType.Operator)
|
||||
{
|
||||
var op = reader.TokenValue;
|
||||
reader.Read();
|
||||
var right = GrabExpression(reader);
|
||||
|
||||
// Create the appropriate comparison expression
|
||||
return op switch
|
||||
{
|
||||
"=" => left == right,
|
||||
"!=" => left != right,
|
||||
"<>" => left != right,
|
||||
"<" => left < right,
|
||||
"<=" => left <= right,
|
||||
">" => left > right,
|
||||
">=" => left >= right,
|
||||
_ => throw new InvalidSyntaxException($"Unsupported comparison operator: {op}")
|
||||
};
|
||||
}
|
||||
|
||||
// If no comparison operator, try to cast as boolean expression
|
||||
if (left is BooleanExpression boolExpr)
|
||||
{
|
||||
return boolExpr;
|
||||
}
|
||||
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {reader.Position}. CASE WHEN condition must be a boolean expression.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes SQL by removing comments and extra whitespace.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement to normalize.</param>
|
||||
/// <returns>The normalized SQL statement.</returns>
|
||||
private static string NormalizeSql(string sql)
|
||||
{
|
||||
var parser = new StatementParser();
|
||||
return parser.NormalizeSql(sql);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
using System.Text;
|
||||
using SqlClauses = Strata.SqlTools.SqlBreakdown.Classes.SqlClauses;
|
||||
using SqlExpressionClause = Strata.SqlTools.SqlBreakdown.Classes.SqlExpressionClause;
|
||||
using SqlServerStatementParser = Strata.SqlTools.Statements.SqlServer.StatementParser;
|
||||
using TokenType = Strata.SqlTools.SqlBreakdown.Enums.SQL.TokenType;
|
||||
|
||||
namespace Strata.SqlTools.Statements.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Provides PostgreSQL-specific SQL parsing utilities for normalizing and cleaning PostgreSQL SQL statements.
|
||||
/// Extends <see cref="Strata.SqlTools.Statements.SqlServer.StatementParser"/> for common operations and handles PostgreSQL-specific syntax
|
||||
/// including double-quoted identifiers, $1, $2 positional parameters, LIMIT/OFFSET support, and RETURNING clause.
|
||||
/// </summary>
|
||||
public class StatementParser : SqlServerStatementParser
|
||||
{
|
||||
#region Constants
|
||||
|
||||
// PostgreSQL-specific keywords
|
||||
public const string KeywordLimit = "LIMIT";
|
||||
public const string KeywordOffset = "OFFSET";
|
||||
public const string KeywordReturning = "RETURNING";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Clause Extraction Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the PostgreSQL-specific setup keywords.
|
||||
/// Includes "CREATE TEMPORARY TABLE", "CREATE TEMP TABLE", and "SET" statements.
|
||||
/// </summary>
|
||||
/// <returns>Array of PostgreSQL-specific setup keywords.</returns>
|
||||
protected override string[] GetSetupKeywords()
|
||||
=> [.. base.GetSetupKeywords(), .. GetPostgreSqlSpecificSetupKeywords()];
|
||||
|
||||
/// <summary>
|
||||
/// Gets PostgreSQL-specific setup keywords.
|
||||
/// </summary>
|
||||
/// <returns>Array of PostgreSQL-specific keywords.</returns>
|
||||
private static string[] GetPostgreSqlSpecificSetupKeywords()
|
||||
=> ["CREATE TEMPORARY TABLE", "CREATE TEMP TABLE", "CREATE SCHEMA", "SET"];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the PostgreSQL-specific finish clause pattern.
|
||||
/// Includes "DROP TABLE", "DROP VIEW", and "DROP SCHEMA" statements.
|
||||
/// </summary>
|
||||
/// <returns>Regex pattern for PostgreSQL finish clauses.</returns>
|
||||
protected override string GetFinishClausePattern()
|
||||
{
|
||||
return @";\s*(DROP\s+(TABLE|VIEW|SCHEMA|TEMPORARY\s+TABLE|TEMP\s+TABLE))";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SELECT Statement Parsing
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of SQL keywords to search for in PostgreSQL statements.
|
||||
/// Includes PostgreSQL-specific LIMIT, OFFSET, and RETURNING keywords.
|
||||
/// </summary>
|
||||
/// <returns>Array of keywords to find.</returns>
|
||||
protected override string[] GetKeywordsToFind()
|
||||
=> [.. base.GetKeywordsToFind(), .. GetPostgreSqlSpecificKeywords()];
|
||||
|
||||
/// <summary>
|
||||
/// Gets PostgreSQL-specific keywords.
|
||||
/// </summary>
|
||||
/// <returns>Array of PostgreSQL-specific keywords.</returns>
|
||||
private static string[] GetPostgreSqlSpecificKeywords()
|
||||
=> [KeywordLimit, KeywordOffset, KeywordReturning];
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a character can start a word (keyword or identifier).
|
||||
/// PostgreSQL: Letters or underscores can start identifiers (like Snowflake).
|
||||
/// </summary>
|
||||
/// <param name="c">The character to check.</param>
|
||||
/// <returns>True if the character is a letter or underscore.</returns>
|
||||
protected override bool IsWordStartCharacter(char c) => char.IsLetter(c) || c == '_';
|
||||
|
||||
/// <summary>
|
||||
/// Handles double-quote character during tokenization.
|
||||
/// PostgreSQL: Treats double-quote as identifier (like Snowflake).
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL statement being tokenized.</param>
|
||||
/// <param name="position">Current position in the SQL string.</param>
|
||||
/// <returns>Token and new position after the token.</returns>
|
||||
protected override ((TokenType type, string value, int position) token, int newPosition) HandleDoubleQuote(string sql, int position)
|
||||
{
|
||||
// PostgreSQL: double-quote is an identifier (like [brackets] in T-SQL)
|
||||
int start = position;
|
||||
position++; // Skip opening quote
|
||||
var identifier = new StringBuilder();
|
||||
while (position < sql.Length && sql[position] != '"')
|
||||
{
|
||||
identifier.Append(sql[position]);
|
||||
position++;
|
||||
}
|
||||
if (position < sql.Length)
|
||||
{
|
||||
position++; // Skip closing quote
|
||||
}
|
||||
|
||||
return ((TokenType.ColumnIdentifier, identifier.ToString(), start), position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Post-processes extracted clauses to handle PostgreSQL-specific LIMIT and OFFSET clauses.
|
||||
/// </summary>
|
||||
/// <param name="clauses">The extracted clauses to post-process.</param>
|
||||
/// <param name="sql">The original SQL statement.</param>
|
||||
/// <param name="clausePositions">Dictionary of keyword positions.</param>
|
||||
protected override void PostProcessClauses(SqlClauses clauses, string sql, Dictionary<string, int> clausePositions)
|
||||
{
|
||||
// PostgreSQL-specific: Append LIMIT/OFFSET to ORDER BY if present
|
||||
var orderByClause = clauses.OrderByClause?.Clause ?? string.Empty;
|
||||
|
||||
if (clausePositions.ContainsKey(KeywordLimit))
|
||||
{
|
||||
var limitStart = clausePositions[KeywordLimit];
|
||||
var limitEnd = clausePositions.Values
|
||||
.Where(v => v > limitStart)
|
||||
.Order()
|
||||
.FirstOrDefault(sql.Length);
|
||||
|
||||
var limitClause = sql.Substring(limitStart, limitEnd - limitStart).Trim();
|
||||
orderByClause = string.IsNullOrEmpty(orderByClause)
|
||||
? limitClause
|
||||
: $"{orderByClause} {limitClause}";
|
||||
}
|
||||
|
||||
if (clausePositions.ContainsKey(KeywordOffset))
|
||||
{
|
||||
var offsetStart = clausePositions[KeywordOffset];
|
||||
var offsetEnd = clausePositions.Values
|
||||
.Where(v => v > offsetStart)
|
||||
.Order()
|
||||
.FirstOrDefault(sql.Length);
|
||||
|
||||
var offsetClause = sql.Substring(offsetStart, offsetEnd - offsetStart).Trim();
|
||||
orderByClause = string.IsNullOrEmpty(orderByClause)
|
||||
? offsetClause
|
||||
: $"{orderByClause} {offsetClause}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(orderByClause))
|
||||
{
|
||||
clauses.OrderByClause = new SqlExpressionClause(splitOnComma: true) { Clause = orderByClause };
|
||||
}
|
||||
|
||||
// Handle RETURNING clause separately (not part of standard SELECT)
|
||||
// RETURNING is typically used with INSERT/UPDATE/DELETE, not SELECT
|
||||
// For SELECT, we'll ignore it; for other statement types, it would be handled differently
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameter Extraction
|
||||
|
||||
/// <summary>
|
||||
/// Extracts PostgreSQL parameters from SQL and populates the parameter dictionary.
|
||||
/// PostgreSQL-specific: Searches for $1, $2, $3, ... syntax and named parameters.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The parameter dictionary to populate.</param>
|
||||
/// <param name="sql">The SQL statement to extract parameters from.</param>
|
||||
public override void ExtractParameters(Dictionary<string, object> parameters, string sql)
|
||||
{
|
||||
if (parameters == null || string.IsNullOrEmpty(sql)) { return; }
|
||||
|
||||
// Extract positional parameters: $1, $2, $3, etc.
|
||||
int index = 0;
|
||||
while ((index = sql.IndexOf('$', index)) != -1)
|
||||
{
|
||||
// Check if followed by a number
|
||||
int numStart = index + 1;
|
||||
if (numStart < sql.Length && char.IsDigit(sql[numStart]))
|
||||
{
|
||||
int numEnd = numStart;
|
||||
while (numEnd < sql.Length && char.IsDigit(sql[numEnd]))
|
||||
{
|
||||
numEnd++;
|
||||
}
|
||||
|
||||
string paramName = sql.Substring(index, numEnd - index); // e.g., "$1", "$2"
|
||||
if (!parameters.ContainsKey(paramName))
|
||||
{
|
||||
parameters[paramName] = null!;
|
||||
}
|
||||
|
||||
index = numEnd;
|
||||
}
|
||||
else
|
||||
{
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Also extract named parameters (e.g., :param or @param for compatibility)
|
||||
base.ExtractParameters(parameters, sql);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
using Strata.SqlTools.SqlBreakdown.Exceptions;
|
||||
using SqlServerStatementReader = Strata.SqlTools.Statements.SqlServer.StatementReader;
|
||||
|
||||
namespace Strata.SqlTools.Statements.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL-specific tokenizer class that reads a string representation of a PostgreSQL SQL statement
|
||||
/// and parses out each part as a token. Handles PostgreSQL's double-quoted identifiers, schema-qualified names,
|
||||
/// single-quoted string literals, positional parameters, and PostgreSQL naming conventions.
|
||||
/// </summary>
|
||||
public class StatementReader : SqlServerStatementReader
|
||||
{
|
||||
public StatementReader(string sqlStatement) : base(sqlStatement)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles PostgreSQL-specific characters: double-quotes (") for delimited identifiers,
|
||||
/// single quotes (') for string literals, dollar sign ($) for positional parameters,
|
||||
/// colon (:) for named parameters, and at-sign (@) for named parameters.
|
||||
/// </summary>
|
||||
/// <returns>True if the character was handled; false otherwise.</returns>
|
||||
/// <summary>
|
||||
/// Attempts to handle additional PostgreSQL-specific characters that the base reader doesn't handle.
|
||||
/// </summary>
|
||||
/// <returns>True if the character was handled; false otherwise.</returns>
|
||||
#pragma warning disable S3776 // Cognitive Complexity - Refactoring this would reduce clarity
|
||||
protected override bool TryHandleAdditionalCharacter()
|
||||
{
|
||||
if (CurrentCharacter == '"')
|
||||
{
|
||||
// PostgreSQL uses double quotes for delimited identifiers (case-sensitive)
|
||||
MovePosition();
|
||||
var quotedIdentifier = GrabStringValue();
|
||||
_currentToken = new Token(TokenType.ColumnIdentifier, quotedIdentifier);
|
||||
if (CurrentCharacter != '"')
|
||||
{
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {Position}. Expected closing double quote.");
|
||||
}
|
||||
MovePosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CurrentCharacter == '\'')
|
||||
{
|
||||
// PostgreSQL uses single quotes for string literals
|
||||
MovePosition();
|
||||
var stringLiteral = GrabStringLiteral();
|
||||
_currentToken = new Token(TokenType.String, stringLiteral);
|
||||
if (CurrentCharacter != '\'')
|
||||
{
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {Position}. Expected closing single quote.");
|
||||
}
|
||||
MovePosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CurrentCharacter == '$')
|
||||
{
|
||||
// PostgreSQL positional parameters: $1, $2, etc.
|
||||
MovePosition();
|
||||
if (char.IsDigit(CurrentCharacter))
|
||||
{
|
||||
var paramNumber = GrabNumberValue();
|
||||
_currentToken = new Token(TokenType.Parameter, $"${paramNumber}");
|
||||
return true;
|
||||
}
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {Position}. Expected digit after $.");
|
||||
}
|
||||
|
||||
if (CurrentCharacter == ':')
|
||||
{
|
||||
// PostgreSQL colon-prefixed named parameters: :userId
|
||||
MovePosition();
|
||||
if (char.IsLetter(CurrentCharacter) || CurrentCharacter == '_')
|
||||
{
|
||||
var paramName = GrabStringValue();
|
||||
_currentToken = new Token(TokenType.Parameter, $":{paramName}");
|
||||
return true;
|
||||
}
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {Position}. Expected identifier after :.");
|
||||
}
|
||||
|
||||
if (CurrentCharacter == '@')
|
||||
{
|
||||
// PostgreSQL at-sign named parameters: @userId (also SQL Server compatible)
|
||||
MovePosition();
|
||||
if (char.IsLetter(CurrentCharacter) || CurrentCharacter == '_')
|
||||
{
|
||||
var paramName = GrabStringValue();
|
||||
_currentToken = new Token(TokenType.Parameter, $"@{paramName}");
|
||||
return true;
|
||||
}
|
||||
throw new InvalidSyntaxException(
|
||||
$"Invalid syntax at position {Position}. Expected identifier after @.");
|
||||
}
|
||||
|
||||
if (CurrentCharacter == '=')
|
||||
{
|
||||
// Handle => operator (used in PostgreSQL for hstore and other operations)
|
||||
MovePosition();
|
||||
if (CurrentCharacter == '>')
|
||||
{
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, "=>");
|
||||
return true;
|
||||
}
|
||||
// Single = is handled as regular operator
|
||||
_currentToken = new Token(TokenType.Operator, "=");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CurrentCharacter == '|')
|
||||
{
|
||||
// Handle || concatenation operator
|
||||
MovePosition();
|
||||
if (CurrentCharacter == '|')
|
||||
{
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, "||");
|
||||
return true;
|
||||
}
|
||||
// Single | is also an operator
|
||||
_currentToken = new Token(TokenType.Operator, "|");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CurrentCharacter == '<')
|
||||
{
|
||||
// Handle <, <=, <>, << operators
|
||||
MovePosition();
|
||||
if (CurrentCharacter == '=')
|
||||
{
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, "<=");
|
||||
return true;
|
||||
}
|
||||
if (CurrentCharacter == '>')
|
||||
{
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, "<>");
|
||||
return true;
|
||||
}
|
||||
if (CurrentCharacter == '<')
|
||||
{
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, "<<");
|
||||
return true;
|
||||
}
|
||||
_currentToken = new Token(TokenType.Operator, "<");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CurrentCharacter == '>')
|
||||
{
|
||||
// Handle >, >=, >> operators
|
||||
MovePosition();
|
||||
if (CurrentCharacter == '=')
|
||||
{
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, ">=");
|
||||
return true;
|
||||
}
|
||||
if (CurrentCharacter == '>')
|
||||
{
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, ">>");
|
||||
return true;
|
||||
}
|
||||
_currentToken = new Token(TokenType.Operator, ">");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CurrentCharacter == '.')
|
||||
{
|
||||
// Handle .. range operator (used in arrays and ranges)
|
||||
// and single . for column qualification (table.column)
|
||||
if (Position + 1 < Length && _sqlStatement[Position + 1] == '.')
|
||||
{
|
||||
MovePosition();
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, "..");
|
||||
return true;
|
||||
}
|
||||
// Single . is used for column qualification (table.column)
|
||||
// Return it as an Operator token
|
||||
MovePosition();
|
||||
_currentToken = new Token(TokenType.Operator, ".");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#pragma warning restore S3776
|
||||
|
||||
/// <summary>
|
||||
/// Handles PostgreSQL-specific identifier prefixes: underscores (_) can start identifiers.
|
||||
/// </summary>
|
||||
/// <returns>True if the character was handled; false otherwise.</returns>
|
||||
protected override bool TryHandleIdentifierPrefix()
|
||||
{
|
||||
if (CurrentCharacter == '_')
|
||||
{
|
||||
var underscoreIdentifier = GrabStringValue();
|
||||
_currentToken = new Token(TokenType.ColumnIdentifier, underscoreIdentifier);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grabs a string literal value between single quotes, handling PostgreSQL's escaped quotes ('').
|
||||
/// </summary>
|
||||
/// <returns>The string literal value without the surrounding quotes.</returns>
|
||||
private string GrabStringLiteral()
|
||||
{
|
||||
var stringValue = new StringBuilder();
|
||||
while (CurrentCharacter != '\'' && CurrentCharacter != char.MinValue)
|
||||
{
|
||||
stringValue.Append(CurrentCharacter);
|
||||
MovePosition();
|
||||
|
||||
// Handle escaped single quotes ('')
|
||||
if (CurrentCharacter == '\'')
|
||||
{
|
||||
var nextPos = Position + 1;
|
||||
if (nextPos < Length && _sqlStatement[nextPos] == '\'')
|
||||
{
|
||||
// Double single-quote is an escape
|
||||
stringValue.Append('\'');
|
||||
MovePosition(); // Skip first quote
|
||||
MovePosition(); // Skip second quote
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stringValue.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.PostgreSql</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - PostgreSQL</Product>
|
||||
<Description>PostgreSQL specific implementations for Strata.SqlTools, including query breakdown, statement parsing, and SQL generation for PostgreSQL dialect with support for parameterized queries using $1, $2 syntax.</Description>
|
||||
<PackageTags>postgresql;sql;query-builder;sql-parser;database;postgres</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with PostgreSQL SQL query parsing, generation, and breakdown support.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,62 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
using SqlServerCommandVisitor = Strata.SqlTools.Visitors.SqlServer.CommandVisitor;
|
||||
|
||||
namespace Strata.SqlTools.Visitors.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the visitor pattern to convert SQL expression objects into PostgreSQL-compatible SQL command strings.
|
||||
/// Inherits from SqlServer.CommandVisitor and overrides only the dialect-specific formatting methods.
|
||||
/// </summary>
|
||||
public class CommandVisitor : SqlServerCommandVisitor
|
||||
{
|
||||
private static int _parameterIndex = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Formats an identifier for PostgreSQL using double-quote quoting.
|
||||
/// </summary>
|
||||
/// <param name="identifier">The identifier to format.</param>
|
||||
/// <returns>The quoted identifier.</returns>
|
||||
protected override string FormatIdentifier(string identifier) => $"\"{identifier}\"";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a parameter name for PostgreSQL using positional parameter syntax.
|
||||
/// Parameters in PostgreSQL are referenced as $1, $2, $3, etc.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name to format.</param>
|
||||
/// <returns>A SQL string in the format "$position" where position is a number.</returns>
|
||||
protected override string FormatParameterName(string parameterName)
|
||||
{
|
||||
// PostgreSQL uses positional parameters: $1, $2, $3, etc.
|
||||
return $"${_parameterIndex++}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a boolean literal for PostgreSQL using TRUE/FALSE keywords.
|
||||
/// </summary>
|
||||
/// <param name="value">The boolean value to format.</param>
|
||||
/// <returns>The string "true" or "false" in lowercase.</returns>
|
||||
protected override string FormatBooleanLiteral(bool value) => value ? "true" : "false";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a string literal for PostgreSQL with proper escaping of single quotes.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value to format.</param>
|
||||
/// <returns>A SQL string literal enclosed in single quotes with escaped quotes.</returns>
|
||||
protected override string FormatStringLiteral(string value)
|
||||
{
|
||||
// PostgreSQL: escape single quotes by doubling them
|
||||
var escaped = value.Replace("'", "''");
|
||||
return $"'{escaped}'";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a case-insensitive LIKE expression for PostgreSQL using ILIKE keyword.
|
||||
/// </summary>
|
||||
/// <param name="likeExpression">The LIKE expression to format.</param>
|
||||
/// <returns>A SQL string in the format "expression ILIKE pattern".</returns>
|
||||
protected override string FormatCaseInsensitiveLike(LikeExpression likeExpression)
|
||||
{
|
||||
return $"{likeExpression.Subject.Accept(this)} ILIKE {likeExpression.Pattern.Accept(this)}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Globalization;
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules;
|
||||
|
||||
public interface IVisitor<out T>
|
||||
{
|
||||
T VisitParameter(Parameter parameter);
|
||||
T VisitProperty(Property property);
|
||||
T VisitCollectionProperty(CollectionProperty collectionProperty);
|
||||
|
||||
T VisitAny(Any Any);
|
||||
|
||||
T VisitLiteral(Literal literalRule);
|
||||
|
||||
T VisitEquals(Equal Equal);
|
||||
T VisitNotEquals(NotEqual Equal);
|
||||
T VisitGreaterThan(GreaterThan GreaterThan);
|
||||
|
||||
T VisitAnd(And And);
|
||||
T VisitOr(Or Or);
|
||||
T VisitWith(With With);
|
||||
}
|
||||
|
||||
public abstract class Visitor : IVisitor<Expression>
|
||||
{
|
||||
public virtual Expression Visit(IVisitable expression) => expression.Accept(this);
|
||||
|
||||
public virtual Expression VisitParameter(Parameter parameter) => parameter;
|
||||
|
||||
public virtual Expression VisitProperty(Property property) => property;
|
||||
|
||||
public virtual Expression VisitCollectionProperty(CollectionProperty collectionProperty) => collectionProperty;
|
||||
|
||||
public virtual Expression VisitAny(Any Any) => new Any(
|
||||
(CollectionProperty)Visit(Any.CollectionProperty),
|
||||
(BoolExpr)Visit(Any.BoolExpr),
|
||||
(Parameter)Visit(Any.PredicateParameter));
|
||||
|
||||
public virtual Expression VisitLiteral(Literal literalRule) => literalRule;
|
||||
|
||||
public virtual Expression VisitEquals(Equal Equal) => Validate(Equal);
|
||||
|
||||
public virtual Expression VisitNotEquals(NotEqual notEqual) => Validate(notEqual);
|
||||
|
||||
public virtual Expression VisitGreaterThan(GreaterThan GreaterThan) => Validate(GreaterThan);
|
||||
|
||||
public virtual Expression VisitAnd(And And) =>
|
||||
new And((BoolExpr)Visit(And.Left), (BoolExpr)Visit(And.Right));
|
||||
|
||||
public virtual Expression VisitOr(Or Or) =>
|
||||
new Or((BoolExpr)Visit(Or.Left), (BoolExpr)Visit(Or.Right));
|
||||
|
||||
public virtual Expression VisitWith(With With) =>
|
||||
new With((BoolExpr)Visit(With.Left), (BoolExpr)Visit(With.Right));
|
||||
|
||||
protected virtual Expression Validate(Comparison comparison)
|
||||
{
|
||||
return comparison.Update(Visit(comparison.Left), Visit(comparison.Right));
|
||||
}
|
||||
}
|
||||
|
||||
public class LocalVisitor : IVisitor<string>
|
||||
{
|
||||
public virtual string Visit(IVisitable expression) => expression.Accept(this);
|
||||
|
||||
public virtual string VisitLiteral(Literal literalRule) => literalRule switch
|
||||
{
|
||||
NumberLiteral number => number.Value.ToString(CultureInfo.InvariantCulture),
|
||||
StringLiteral stringRule => $"\"{stringRule.Value}\"",
|
||||
not null => literalRule.Value.ToString() ?? string.Empty,
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
public virtual string VisitEquals(Equal Equal)
|
||||
{
|
||||
return $"{Equal.Left.Accept(this)} == {Equal.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitNotEquals(NotEqual notEqual)
|
||||
{
|
||||
return $"{notEqual.Left.Accept(this)} != {notEqual.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitGreaterThan(GreaterThan GreaterThan)
|
||||
{
|
||||
return $"{GreaterThan.Left.Accept(this)} > {GreaterThan.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitAnd(And And)
|
||||
{
|
||||
return $"{And.Left.Accept(this)} && {And.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitOr(Or Or)
|
||||
{
|
||||
return $"{Or.Left.Accept(this)} || {Or.Right.Accept(this)}";
|
||||
}
|
||||
|
||||
public virtual string VisitWith(With With)
|
||||
{
|
||||
// just converting it to an AND expression for now
|
||||
var and = new And(With.Left, With.Right);
|
||||
return and.Accept(this);
|
||||
//throw new NotImplementedException("not sure what to do with 'WITH' expressions yet");
|
||||
}
|
||||
|
||||
private bool TryGetCollectionItemProperty(Expression Expression, out Property? property)
|
||||
{
|
||||
property = null;
|
||||
|
||||
if (Expression is not IBinary binary)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binary.Left is not Property Property)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Property.Expression is not CollectionProperty collection)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
property = Property;
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual string VisitParameter(Parameter parameter) => $"{parameter.ParameterName}";
|
||||
|
||||
public virtual string VisitProperty(Property property)
|
||||
{
|
||||
return property.Expression is null
|
||||
? $"{property.PropertyName}"
|
||||
: $"{property.Expression.Accept(this)}.{property.PropertyName}";
|
||||
}
|
||||
|
||||
public virtual string VisitCollectionProperty(CollectionProperty collectionProperty) => VisitProperty(collectionProperty);
|
||||
|
||||
public virtual string VisitAny(Any Any)
|
||||
{
|
||||
return $"{Any.CollectionProperty.Accept(this)}.Any({Any.PredicateParameter.Accept(this)} => {Any.BoolExpr.Accept(this)})";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a logical AND operation between two BoolExpr expressions.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Left} AND {Right}")]
|
||||
public class And : Logical
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="And"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public And(BoolExpr left, BoolExpr right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitAnd(this);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an ANY expression that checks if any element in a collection satisfies a condition.
|
||||
/// </summary>
|
||||
public class Any : BoolExpr
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection property being evaluated.
|
||||
/// </summary>
|
||||
public CollectionProperty CollectionProperty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the BoolExpr expression that defines the condition to check.
|
||||
/// </summary>
|
||||
public BoolExpr BoolExpr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter used in the predicate expression.
|
||||
/// </summary>
|
||||
public Parameter PredicateParameter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Any"/> class with a function.
|
||||
/// </summary>
|
||||
/// <param name="collectionProperty">The collection property to evaluate.</param>
|
||||
/// <param name="func">A function that defines the condition to check for each element.</param>
|
||||
public Any(CollectionProperty collectionProperty, Func<Parameter, BoolExpr> func)
|
||||
{
|
||||
CollectionProperty = collectionProperty;
|
||||
PredicateParameter = new Parameter("p");
|
||||
BoolExpr = func(PredicateParameter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Any"/> class with a BoolExpr expression.
|
||||
/// </summary>
|
||||
/// <param name="collectionProperty">The collection property to evaluate.</param>
|
||||
/// <param name="boolExpr">The BoolExpr expression defining the condition.</param>
|
||||
public Any(CollectionProperty collectionProperty, BoolExpr boolExpr)
|
||||
: this(collectionProperty, boolExpr, new Parameter("p"))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Any"/> class.
|
||||
/// </summary>
|
||||
/// <param name="collectionProperty">The collection property to evaluate.</param>
|
||||
/// <param name="boolExpr">The BoolExpr expression defining the condition.</param>
|
||||
/// <param name="predicateParameter">The parameter used in the predicate expression.</param>
|
||||
public Any(CollectionProperty collectionProperty, BoolExpr boolExpr, Parameter predicateParameter)
|
||||
{
|
||||
CollectionProperty = collectionProperty;
|
||||
BoolExpr = boolExpr;
|
||||
PredicateParameter = predicateParameter;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitAny(this);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an expression that evaluates to a BoolExpr value (true/false).
|
||||
/// </summary>
|
||||
public abstract class BoolExpr : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a logical AND expression combining two BoolExpr expressions.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>An AND expression combining both operands.</returns>
|
||||
public static BoolExpr operator &(BoolExpr left, BoolExpr right) => new And(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a logical OR expression combining two BoolExpr expressions.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>An OR expression combining both operands.</returns>
|
||||
public static BoolExpr operator |(BoolExpr left, BoolExpr right) => new Or(left, right);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a collection property access in a rule expression.
|
||||
/// </summary>
|
||||
public class CollectionProperty : Property
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionProperty"/> class.
|
||||
/// </summary>
|
||||
/// <param name="expression">The containing expression.</param>
|
||||
/// <param name="propertyName">The name of the collection property.</param>
|
||||
public CollectionProperty(Expression? expression, string propertyName) : base(expression, propertyName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitCollectionProperty(this);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ANY expression that checks if any element in the collection satisfies a condition.
|
||||
/// </summary>
|
||||
/// <param name="func">A function that defines the condition to check for each element.</param>
|
||||
/// <returns>An ANY expression.</returns>
|
||||
public Any Any(Func<Parameter, BoolExpr> func)
|
||||
{
|
||||
var parameter = new Parameter("p");
|
||||
var BoolExpr = func(parameter);
|
||||
return new Any(this, BoolExpr, parameter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a comparison operation between two expressions.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Left} {Type} {Right}")]
|
||||
public abstract class Comparison : BoolExpr, IBinary
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the left operand of the comparison.
|
||||
/// </summary>
|
||||
public Expression Left { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right operand of the comparison.
|
||||
/// </summary>
|
||||
public Expression Right { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of comparison operation.
|
||||
/// </summary>
|
||||
public abstract Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Comparison"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
protected Comparison(Expression left, Expression right)
|
||||
{
|
||||
Left = left;
|
||||
Right = right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new comparison expression with updated operands.
|
||||
/// </summary>
|
||||
/// <param name="left">The new left operand.</param>
|
||||
/// <param name="right">The new right operand.</param>
|
||||
/// <returns>A new comparison expression or this instance if operands are unchanged.</returns>
|
||||
public Expression Update(Expression left, Expression right)
|
||||
{
|
||||
if (ReferenceEquals(left, Left) && ReferenceEquals(right, Right))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
return Create(left, right, Type);
|
||||
}
|
||||
|
||||
private static Comparison Create(Expression left, Expression right, Type Type)
|
||||
{
|
||||
return Type switch
|
||||
{
|
||||
Type.Equal => new Equal(left, right),
|
||||
Type.NotEqual => new NotEqual(left, right),
|
||||
Type.GreaterThan => new GreaterThan(left, right),
|
||||
|
||||
_ => throw new NotImplementedException("not yet")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an equality comparison between two expressions.
|
||||
/// </summary>
|
||||
public class Equal : Comparison
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Type Type => Type.Equal;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Equal"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public Equal(Expression left, Expression right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitEquals(this);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Provides implicit conversion operators and comparison operators for rule expressions.
|
||||
/// </summary>
|
||||
#pragma warning disable CS0660, CS0661
|
||||
public partial class Expression
|
||||
#pragma warning restore CS0660, CS0661
|
||||
{
|
||||
/// <summary>
|
||||
/// Implicitly converts a decimal value to a rule expression.
|
||||
/// </summary>
|
||||
/// <param name="value">The decimal value to convert.</param>
|
||||
public static implicit operator Expression(decimal value) => new NumberLiteral(value);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a string value to a rule expression.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value to convert.</param>
|
||||
public static implicit operator Expression(string value) => new StringLiteral(value);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an equality comparison rule expression.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>An equality comparison rule expression.</returns>
|
||||
public static Comparison operator ==(Expression left, Expression right) => new Equal(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a not-equal comparison rule expression.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>A not-equal comparison rule expression.</returns>
|
||||
public static Comparison operator !=(Expression left, Expression right) => new NotEqual(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an equality comparison rule expression.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
/// <returns>An equality comparison rule expression.</returns>
|
||||
public static Equal Equal(Expression left, Expression right) => new(left, right);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all rule expressions.
|
||||
/// </summary>
|
||||
#pragma warning disable CS0660, CS0661
|
||||
public abstract partial class Expression : IVisitable
|
||||
#pragma warning restore CS0660, CS0661
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a visitor and allows it to process this rule expression.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">The visitor to accept.</param>
|
||||
/// <returns>The result of the visitor's processing.</returns>
|
||||
public abstract T Accept<T>(IVisitor<T> visitor);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a greater-than comparison between two expressions.
|
||||
/// </summary>
|
||||
public class GreaterThan : Comparison
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Type Type => Type.GreaterThan;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GreaterThan"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public GreaterThan(Expression left, Expression right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitGreaterThan(this);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a binary rule expression with left and right operands.
|
||||
/// </summary>
|
||||
public interface IBinary : IVisitable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the left operand.
|
||||
/// </summary>
|
||||
public Expression Left { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right operand.
|
||||
/// </summary>
|
||||
public Expression Right { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of rule expression.
|
||||
/// </summary>
|
||||
public Type Type { get; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an object that can be visited by a rule visitor implementing the visitor pattern.
|
||||
/// </summary>
|
||||
public interface IVisitable
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a visitor and allows it to process this visitable object.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the visitor.</typeparam>
|
||||
/// <param name="visitor">The visitor to accept.</param>
|
||||
/// <returns>The result of the visitor's processing.</returns>
|
||||
T Accept<T>(IVisitor<T> visitor);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a literal value in a rule expression.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("\\{{Value}\\}")]
|
||||
public class Literal : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the literal value.
|
||||
/// </summary>
|
||||
public object Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Literal"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The literal value.</param>
|
||||
public Literal(object value) => Value = value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitLiteral(this);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for typed literal rule expressions.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the literal value.</typeparam>
|
||||
public abstract class Literal<TValue> : Literal
|
||||
where TValue : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the strongly-typed literal value.
|
||||
/// </summary>
|
||||
public new TValue Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Literal{TValue}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The literal value.</param>
|
||||
protected Literal(TValue value) : base(value) => Value = value;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a logical operation on two BoolExpr input expressions (e.g., AND, OR).
|
||||
/// </summary>
|
||||
public abstract class Logical : BoolExpr
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the left operand of the logical expression.
|
||||
/// </summary>
|
||||
public BoolExpr Left { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right operand of the logical expression.
|
||||
/// </summary>
|
||||
public BoolExpr Right { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Logical"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
protected Logical(BoolExpr left, BoolExpr right)
|
||||
{
|
||||
Left = left;
|
||||
Right = right;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Parses markdown/LaTeX mathematical expressions and converts them to Expression objects.
|
||||
/// Supports parsing of logical operations, comparisons, properties, and literals.
|
||||
/// </summary>
|
||||
public static class Markdown
|
||||
{
|
||||
private static readonly Dictionary<string, Func<Expression, Expression, BoolExpr>> LogicalOperators = new()
|
||||
{
|
||||
{ "\\land", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "\\lor", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "\\wedge", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "\\vee", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "AND", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
|
||||
{ "OR", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, Func<Expression, Expression, Comparison>> ComparisonOperators = new()
|
||||
{
|
||||
{ "=", (left, right) => new Equal(left, right) },
|
||||
{ "\\neq", (left, right) => new NotEqual(left, right) },
|
||||
{ "!=", (left, right) => new NotEqual(left, right) },
|
||||
{ ">", (left, right) => new GreaterThan(left, right) },
|
||||
{ "\\gt", (left, right) => new GreaterThan(left, right) },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Parses a markdown/LaTeX string into an Expression object.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The markdown/LaTeX string to parse.</param>
|
||||
/// <returns>The parsed Expression object.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the markdown cannot be parsed.</exception>
|
||||
public static Expression Parse(string markdown)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(markdown))
|
||||
{
|
||||
throw new ArgumentException("Markdown cannot be null or empty", nameof(markdown));
|
||||
}
|
||||
|
||||
// Remove common markdown delimiters
|
||||
markdown = markdown.Trim();
|
||||
markdown = StripMarkdownDelimiters(markdown);
|
||||
|
||||
return ParseExpression(markdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a markdown/LaTeX string into an Expression object.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The markdown/LaTeX string to parse.</param>
|
||||
/// <param name="expression">The parsed Expression object if successful.</param>
|
||||
/// <returns>True if parsing was successful, false otherwise.</returns>
|
||||
public static bool TryParse(string markdown, out Expression? expression)
|
||||
{
|
||||
try
|
||||
{
|
||||
expression = Parse(markdown);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
expression = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string StripMarkdownDelimiters(string text)
|
||||
{
|
||||
// Remove $...$ or $$...$$ delimiters
|
||||
text = Regex.Replace(text, @"^\$\$?\s*", "");
|
||||
text = Regex.Replace(text, @"\s*\$\$?$", "");
|
||||
|
||||
// Remove ```math...``` code fence
|
||||
text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline);
|
||||
text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline);
|
||||
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
private static Expression ParseExpression(string text)
|
||||
{
|
||||
text = text.Trim();
|
||||
|
||||
// Try to parse logical operations (lowest precedence)
|
||||
var logicalExpr = TryParseLogicalOperation(text);
|
||||
if (logicalExpr is not null)
|
||||
{
|
||||
return logicalExpr;
|
||||
}
|
||||
|
||||
// Try to parse comparison operations
|
||||
var comparisonExpr = TryParseComparison(text);
|
||||
if (comparisonExpr is not null)
|
||||
{
|
||||
return comparisonExpr;
|
||||
}
|
||||
|
||||
// Handle parentheses
|
||||
var parenthesisExpr = TryParseParentheses(text);
|
||||
if (parenthesisExpr is not null)
|
||||
{
|
||||
return parenthesisExpr;
|
||||
}
|
||||
|
||||
// Parse property, literal, or other atomic expressions
|
||||
return ParseAtomicExpression(text);
|
||||
}
|
||||
|
||||
private static Expression? TryParseLogicalOperation(string text)
|
||||
{
|
||||
foreach (var op in LogicalOperators.Keys)
|
||||
{
|
||||
var parts = SplitByOperator(text, op);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var left = ParseExpression(parts[0]);
|
||||
var right = ParseExpression(parts[1]);
|
||||
return LogicalOperators[op](left, right);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression? TryParseComparison(string text)
|
||||
{
|
||||
foreach (var op in ComparisonOperators.Keys)
|
||||
{
|
||||
var parts = SplitByOperator(text, op);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var left = ParseExpression(parts[0]);
|
||||
var right = ParseExpression(parts[1]);
|
||||
return ComparisonOperators[op](left, right);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression? TryParseParentheses(string text)
|
||||
{
|
||||
// Handle regular parentheses
|
||||
if (text.StartsWith('(') && text.EndsWith(')'))
|
||||
{
|
||||
var inner = text.Substring(1, text.Length - 2);
|
||||
if (IsBalanced(inner))
|
||||
{
|
||||
return ParseExpression(inner);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle LaTeX \left( and \right)
|
||||
if (text.StartsWith("\\left(") && text.EndsWith("\\right)"))
|
||||
{
|
||||
var inner = text.Substring(6, text.Length - 13);
|
||||
if (IsBalanced(inner))
|
||||
{
|
||||
return ParseExpression(inner);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression ParseAtomicExpression(string text)
|
||||
{
|
||||
// Try parsing as property access
|
||||
var propertyExpr = TryParseProperty(text);
|
||||
if (propertyExpr is not null)
|
||||
{
|
||||
return propertyExpr;
|
||||
}
|
||||
|
||||
// Try parsing as literal
|
||||
var literalExpr = TryParseLiteral(text);
|
||||
if (literalExpr is not null)
|
||||
{
|
||||
return literalExpr;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unable to parse expression: {text}");
|
||||
}
|
||||
|
||||
private static Expression? TryParseProperty(string text)
|
||||
{
|
||||
// Parse property access (e.g., x.PropertyName or \text{x.PropertyName})
|
||||
var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
|
||||
if (propertyMatch.Success)
|
||||
{
|
||||
return new Property(propertyMatch.Groups[1].Value, propertyMatch.Groups[2].Value);
|
||||
}
|
||||
|
||||
// Parse \text{...} property access
|
||||
var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$");
|
||||
if (textMatch.Success)
|
||||
{
|
||||
var textContent = textMatch.Groups[1].Value;
|
||||
var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
|
||||
if (propMatch.Success)
|
||||
{
|
||||
return new Property(propMatch.Groups[1].Value, propMatch.Groups[2].Value);
|
||||
}
|
||||
|
||||
// Check for boolean literals in \text{} format
|
||||
if (textContent.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Literal(true);
|
||||
}
|
||||
|
||||
if (textContent.Equals("false", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Literal(false);
|
||||
}
|
||||
|
||||
// Single property name
|
||||
if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||
{
|
||||
return new Property(textContent);
|
||||
}
|
||||
|
||||
// String literal
|
||||
return new StringLiteral(textContent);
|
||||
}
|
||||
|
||||
// Check for boolean literals before simple property
|
||||
if (text.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Literal(true);
|
||||
}
|
||||
|
||||
if (text.Equals("false", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Literal(false);
|
||||
}
|
||||
|
||||
// Parse simple property without parameter
|
||||
if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||
{
|
||||
return new Property(text);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression? TryParseLiteral(string text)
|
||||
{
|
||||
// Parse string literals (quoted)
|
||||
var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$");
|
||||
if (stringMatch.Success)
|
||||
{
|
||||
return new StringLiteral(stringMatch.Groups[1].Value);
|
||||
}
|
||||
|
||||
// Parse empty string literals
|
||||
if (text == "\"\"" || text == "''")
|
||||
{
|
||||
return new StringLiteral(string.Empty);
|
||||
}
|
||||
|
||||
// Parse numeric literals
|
||||
if (int.TryParse(text, out var intValue))
|
||||
{
|
||||
return new NumberLiteral(intValue);
|
||||
}
|
||||
|
||||
if (decimal.TryParse(text, out var decimalValue))
|
||||
{
|
||||
return new NumberLiteral(decimalValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string[] SplitByOperator(string text, string op)
|
||||
{
|
||||
var result = new List<string>();
|
||||
int depth = 0;
|
||||
int lastIndex = 0;
|
||||
int i = 0;
|
||||
|
||||
while (i < text.Length)
|
||||
{
|
||||
i = ProcessParentheses(text, i, ref depth);
|
||||
if (i >= text.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we found the operator at depth 0
|
||||
if (depth == 0 && i + op.Length <= text.Length && TryMatchOperator(text, i, op))
|
||||
{
|
||||
result.Add(text.Substring(lastIndex, i - lastIndex).Trim());
|
||||
lastIndex = i + op.Length;
|
||||
i += op.Length;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if (result.Count == 0)
|
||||
{
|
||||
return new[] { text };
|
||||
}
|
||||
|
||||
result.Add(text.Substring(lastIndex).Trim());
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static int ProcessParentheses(string text, int index, ref int depth)
|
||||
{
|
||||
// Track parentheses depth
|
||||
if (text[index] == '(' || (index + 5 < text.Length && text.Substring(index, 6) == "\\left("))
|
||||
{
|
||||
depth++;
|
||||
if (text[index] == '\\')
|
||||
{
|
||||
return index + 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (text[index] == ')' || (index + 6 < text.Length && text.Substring(index, 7) == "\\right)"))
|
||||
{
|
||||
depth--;
|
||||
if (text[index] == '\\')
|
||||
{
|
||||
return index + 7;
|
||||
}
|
||||
else
|
||||
{
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static bool TryMatchOperator(string text, int index, string op)
|
||||
{
|
||||
var substring = text.Substring(index, op.Length);
|
||||
if (substring != op)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure it's a separate operator, not part of a larger token
|
||||
bool validBefore = (index == 0 || char.IsWhiteSpace(text[index - 1]) || text[index] == '\\');
|
||||
bool validAfter = (index + op.Length >= text.Length || char.IsWhiteSpace(text[index + op.Length]));
|
||||
|
||||
return validBefore && validAfter;
|
||||
}
|
||||
|
||||
private static bool IsBalanced(string text)
|
||||
{
|
||||
int depth = 0;
|
||||
int i = 0;
|
||||
|
||||
while (i < text.Length)
|
||||
{
|
||||
if (text[i] == '(')
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (text[i] == ')')
|
||||
{
|
||||
depth--;
|
||||
if (depth < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (i + 5 < text.Length && text.Substring(i, 6) == "\\left(")
|
||||
{
|
||||
depth++;
|
||||
i += 5;
|
||||
}
|
||||
else if (i + 6 < text.Length && text.Substring(i, 7) == "\\right)")
|
||||
{
|
||||
depth--;
|
||||
if (depth < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
i += 6;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return depth == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a not-equal comparison between two expressions.
|
||||
/// </summary>
|
||||
public class NotEqual : Comparison
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Type Type => Type.NotEqual;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotEqual"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public NotEqual(Expression left, Expression right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitNotEquals(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a numeric literal value in a rule expression.
|
||||
/// </summary>
|
||||
public class NumberLiteral : Literal<decimal>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref=" NumberLiteral"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The numeric value.</param>
|
||||
public NumberLiteral(decimal value) : base(value) { }
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a <see cref=" NumberLiteral"/> to a decimal value.
|
||||
/// </summary>
|
||||
/// <param name="numberExp">The number expression to convert.</param>
|
||||
public static implicit operator decimal(NumberLiteral numberExp) => numberExp.Value;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a logical OR operation between two BoolExpr expressions.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Left} OR {Right}")]
|
||||
public class Or : Logical
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Or"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public Or(BoolExpr left, BoolExpr right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitOr(this);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a parameter in a rule expression.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{ParameterName}")]
|
||||
public class Parameter : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the parameter.
|
||||
/// </summary>
|
||||
public string ParameterName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Parameter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter.</param>
|
||||
public Parameter(string parameterName) => ParameterName = parameterName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitParameter(this);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a property expression for accessing a property on this parameter.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
/// <returns>A property expression.</returns>
|
||||
public Property Property(string propertyName) => new(this, propertyName);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a collection property expression for accessing a collection property on this parameter.
|
||||
/// </summary>
|
||||
/// <param name="collectionPropertyName">The name of the collection property.</param>
|
||||
/// <returns>A collection property expression.</returns>
|
||||
public CollectionProperty CollectionProperty(string collectionPropertyName) => new(this, collectionPropertyName);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property access in a rule expression.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("\\{{Expression,nq}.{PropertyName,nq}\\}")]
|
||||
public class Property : Expression
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the containing object of the field or property.
|
||||
/// </summary>
|
||||
public Expression? Expression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the property.
|
||||
/// </summary>
|
||||
public string PropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Property"/> class with no containing expression.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
public Property(string propertyName) : this((Expression?)null, propertyName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Property"/> class with a parameter name.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter.</param>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
public Property(string parameterName, string propertyName) : this(new Parameter(parameterName), propertyName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Property"/> class.
|
||||
/// </summary>
|
||||
/// <param name="expression">The containing expression.</param>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="propertyName"/> is null.</exception>
|
||||
public Property(Expression? expression, string propertyName)
|
||||
{
|
||||
Expression = expression;
|
||||
// maybe do some regex validation for args to ensure it's not a bogus name (no whitespace, no punctuation marks, etc)
|
||||
PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitProperty(this);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a string literal value in a rule expression.
|
||||
/// </summary>
|
||||
public class StringLiteral : Literal<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringLiteral"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value.</param>
|
||||
public StringLiteral(string value) : base(value) { }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the types of rule expressions for comparisons and operations.
|
||||
/// </summary>
|
||||
public enum Type
|
||||
{
|
||||
/// <summary>Equality comparison.</summary>
|
||||
Equal,
|
||||
/// <summary>Inequality comparison.</summary>
|
||||
NotEqual,
|
||||
/// <summary>Greater than comparison.</summary>
|
||||
GreaterThan,
|
||||
/// <summary>Greater than or equal comparison.</summary>
|
||||
GreaterThanOrEqual,
|
||||
/// <summary>Less than comparison.</summary>
|
||||
LessThan,
|
||||
/// <summary>Less than or equal comparison.</summary>
|
||||
LessThanOrEqual,
|
||||
|
||||
/// <summary>In operation (value in set).</summary>
|
||||
In,
|
||||
/// <summary>None equal operation.</summary>
|
||||
NoneEqual,
|
||||
/// <summary>Exclude operation.</summary>
|
||||
Exclude,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a WITH operation for sequential rule evaluation.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Left} WITH {Right}")]
|
||||
public class With : Logical
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="With"/> class.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand.</param>
|
||||
/// <param name="right">The right operand.</param>
|
||||
public With(BoolExpr left, BoolExpr right) : base(left, right) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Accept<T>(IVisitor<T> visitor) => visitor.VisitWith(this);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rule group where all rules must evaluate to true (logical AND).
|
||||
/// </summary>
|
||||
public class And : Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges two BoolExpr expressions using logical AND.
|
||||
/// </summary>
|
||||
/// <param name="left">The left BoolExpr expression.</param>
|
||||
/// <param name="right">The right BoolExpr expression.</param>
|
||||
/// <returns>An AND expression combining both expressions.</returns>
|
||||
protected override BoolExpr Merge(BoolExpr left, BoolExpr right) => new Expression.And(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="And"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rules">The collection of rules to include in this AND group.</param>
|
||||
public And(IEnumerable<IRule> rules) : base(rules) { }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for rule groups that provides common functionality for grouping and merging rules.
|
||||
/// </summary>
|
||||
public abstract class Base : IGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// The internal list of rules in this group.
|
||||
/// </summary>
|
||||
protected readonly List<IRule> RuleList;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of rules in this group.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<IRule> Rules => RuleList;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the merged BoolExpr expression for all rules in this group.
|
||||
/// </summary>
|
||||
public BoolExpr Expression => GetExpressions().Aggregate(Merge);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expressions from all rules in this group.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of BoolExpr rule expressions.</returns>
|
||||
protected virtual IEnumerable<BoolExpr> GetExpressions() => RuleList.Select(r => r.Expression);
|
||||
|
||||
/// <summary>
|
||||
/// Merges two BoolExpr expressions according to the group's logic.
|
||||
/// </summary>
|
||||
/// <param name="left">The left BoolExpr expression.</param>
|
||||
/// <param name="right">The right BoolExpr expression.</param>
|
||||
/// <returns>The merged BoolExpr expression.</returns>
|
||||
protected abstract BoolExpr Merge(BoolExpr left, BoolExpr right);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Base"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rules">The collection of rules to include in this group.</param>
|
||||
protected Base(IEnumerable<IRule> rules)
|
||||
{
|
||||
RuleList = rules.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a group of rules that can be evaluated together.
|
||||
/// </summary>
|
||||
public interface IGroup : IRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of rules in this group.
|
||||
/// </summary>
|
||||
IReadOnlyCollection<IRule> Rules { get; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rule group where at least one rule must evaluate to true (logical OR).
|
||||
/// </summary>
|
||||
public class Or : Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges two BoolExpr expressions using logical OR.
|
||||
/// </summary>
|
||||
/// <param name="left">The left BoolExpr expression.</param>
|
||||
/// <param name="right">The right BoolExpr expression.</param>
|
||||
/// <returns>An OR expression combining both expressions.</returns>
|
||||
protected override BoolExpr Merge(BoolExpr left, BoolExpr right) => new Expression.Or(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Or"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rules">The collection of rules to include in this OR group.</param>
|
||||
public Or(IEnumerable<IRule> rules) : base(rules) { }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rule group with sequential rule evaluation (WITH semantics).
|
||||
/// </summary>
|
||||
public class With : Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the expressions from all rules, potentially with ordering applied.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of BoolExpr rule expressions.</returns>
|
||||
protected override IEnumerable<BoolExpr> GetExpressions()
|
||||
{
|
||||
// do some ordering here??
|
||||
return base.GetExpressions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges two BoolExpr expressions using WITH semantics.
|
||||
/// </summary>
|
||||
/// <param name="left">The left BoolExpr expression.</param>
|
||||
/// <param name="right">The right BoolExpr expression.</param>
|
||||
/// <returns>A WITH expression combining both expressions.</returns>
|
||||
protected override BoolExpr Merge(BoolExpr left, BoolExpr right) => new Expression.With(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="With"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rules">The collection of rules to include in this WITH group.</param>
|
||||
public With(IEnumerable<IRule> rules) : base(rules) { }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rule that contains a BoolExpr expression for evaluation.
|
||||
/// </summary>
|
||||
public interface IRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the BoolExpr expression that defines the rule logic.
|
||||
/// </summary>
|
||||
public BoolExpr Expression { get; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
using Strata.SqlTools.Rules.Rule.Groups;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a complete set of rules with a unique identifier and a root rule group.
|
||||
/// </summary>
|
||||
public class RuleSet : IGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for this rule set.
|
||||
/// </summary>
|
||||
public Guid RuleSetId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root rule group containing all rules in this set.
|
||||
/// </summary>
|
||||
public IGroup RootRuleGroup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the merged BoolExpr expression from the root rule group.
|
||||
/// </summary>
|
||||
public BoolExpr Expression => RootRuleGroup.Expression;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of rules from the root rule group.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<IRule> Rules => RootRuleGroup.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleSet"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rootRuleGroup">The root rule group containing all rules.</param>
|
||||
/// <param name="ruleSetId">The unique identifier for this rule set.</param>
|
||||
public RuleSet(IGroup rootRuleGroup, Guid ruleSetId)
|
||||
{
|
||||
RootRuleGroup = rootRuleGroup;
|
||||
RuleSetId = ruleSetId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all single rules from the rule set, recursively traversing all rule groups.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of all single rules in the rule set.</returns>
|
||||
public IEnumerable<SingleRule> GetAllSingleRules() => GetAllSingleRules(RootRuleGroup);
|
||||
|
||||
/// <summary>
|
||||
/// Recursively gets all single rules from a rule group and its nested groups.
|
||||
/// </summary>
|
||||
/// <param name="group">The rule group to traverse.</param>
|
||||
/// <returns>An enumerable of all single rules found in the group.</returns>
|
||||
private static IEnumerable<SingleRule> GetAllSingleRules(IGroup group)
|
||||
{
|
||||
var rules = new List<SingleRule>();
|
||||
foreach (var rule in group.Rules)
|
||||
{
|
||||
switch (rule)
|
||||
{
|
||||
case Base childGroup:
|
||||
rules.AddRange(GetAllSingleRules(childGroup));
|
||||
break;
|
||||
case SingleRule single:
|
||||
rules.Add(single);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Strata.SqlTools.Rules.Rule.Expression;
|
||||
|
||||
namespace Strata.SqlTools.Rules.Rule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single rule with a name and a BoolExpr expression.
|
||||
/// </summary>
|
||||
public class SingleRule : IRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the rule.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the BoolExpr expression that defines the rule logic.
|
||||
/// </summary>
|
||||
public BoolExpr Expression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property of the input that is used in the <see cref="Expression"/>.
|
||||
/// Returns null if the Expression does not use a property from the input.
|
||||
/// </summary>
|
||||
public Property? Property
|
||||
{
|
||||
get
|
||||
{
|
||||
return Expression switch
|
||||
{
|
||||
Comparison { Left: Property property } => property,
|
||||
Any any => any.CollectionProperty,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleRule"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the rule.</param>
|
||||
/// <param name="expression">The BoolExpr expression that defines the rule logic.</param>
|
||||
public SingleRule(string name, BoolExpr expression)
|
||||
{
|
||||
Name = name;
|
||||
Expression = expression;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Dynamic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using RulesEngine.Interfaces;
|
||||
using RulesEngine.Models;
|
||||
using Strata.SqlTools.Rules.Rule;
|
||||
|
||||
[assembly: InternalsVisibleTo("Strata.SqlTools.Rules.Tests", AllInternalsVisible = true)]
|
||||
|
||||
namespace Strata.SqlTools.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Engine for executing rule sets by translating them to the Microsoft RulesEngine format.
|
||||
/// </summary>
|
||||
internal class RuleSetEngine
|
||||
{
|
||||
private readonly IRulesEngine _innerRulesEngine;
|
||||
private readonly RuleTranslator _translator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleSetEngine"/> class with default dependencies.
|
||||
/// </summary>
|
||||
public RuleSetEngine() : this(new RulesEngine.RulesEngine(), new RuleTranslator())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleSetEngine"/> class with specified dependencies.
|
||||
/// </summary>
|
||||
/// <param name="innerRulesEngine">The underlying rules engine to use for execution.</param>
|
||||
/// <param name="translator">The translator to convert rule sets to workflow format.</param>
|
||||
internal RuleSetEngine(IRulesEngine innerRulesEngine, RuleTranslator translator)
|
||||
{
|
||||
_innerRulesEngine = innerRulesEngine;
|
||||
_translator = translator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all rules in the rule set against the provided input.
|
||||
/// </summary>
|
||||
/// <param name="ruleSet">The rule set to execute.</param>
|
||||
/// <param name="input">The input object to evaluate against the rules.</param>
|
||||
/// <returns>A task representing the asynchronous operation, containing true if all rules passed, false otherwise.</returns>
|
||||
public async ValueTask<bool> RunRules(RuleSet ruleSet, object input)
|
||||
{
|
||||
var name = ruleSet.RuleSetId.ToString();
|
||||
|
||||
if (!_innerRulesEngine.ContainsWorkflow(name))
|
||||
{
|
||||
var workflow = _translator.TranslateRuleSet(ruleSet);
|
||||
workflow.WorkflowName = name;
|
||||
_innerRulesEngine.AddOrUpdateWorkflow(workflow);
|
||||
}
|
||||
|
||||
var result = await _innerRulesEngine.ExecuteAllRulesAsync(name, new RuleParameter("input", input));
|
||||
|
||||
var success = result?.TrueForAll(tree => tree.IsSuccess) ?? false;
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an object to an ExpandoObject by copying all public properties.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to convert.</param>
|
||||
/// <returns>An ExpandoObject containing all properties from the source object.</returns>
|
||||
private static ExpandoObject ConvertObjectToExpando(object obj)
|
||||
{
|
||||
var expando = new ExpandoObject();
|
||||
var dictionary = expando as IDictionary<string, object?>;
|
||||
foreach (var property in obj.GetType().GetProperties())
|
||||
{
|
||||
dictionary.Add(property.Name, property.GetValue(obj));
|
||||
}
|
||||
|
||||
return expando;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates rule expressions from Strata Domain-Specific-Language to Microsoft.RulesEngine format.
|
||||
/// </summary>
|
||||
internal class RuleTranslator
|
||||
{
|
||||
private readonly IVisitor<string> _localRuleVisitor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleTranslator"/> class with a default local rule visitor.
|
||||
/// </summary>
|
||||
public RuleTranslator() : this(new LocalVisitor())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuleTranslator"/> class with a specified rule visitor.
|
||||
/// </summary>
|
||||
/// <param name="localRuleVisitor">The rule visitor to use for translating expressions to strings.</param>
|
||||
public RuleTranslator(IVisitor<string> localRuleVisitor)
|
||||
{
|
||||
_localRuleVisitor = localRuleVisitor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates a rule set to a Microsoft RulesEngine workflow.
|
||||
/// </summary>
|
||||
/// <param name="ruleSet">The rule set to translate.</param>
|
||||
/// <returns>A workflow containing the translated rules.</returns>
|
||||
public Workflow TranslateRuleSet(RuleSet ruleSet)
|
||||
{
|
||||
var workflowRules = new List<RulesEngine.Models.Rule>();
|
||||
|
||||
var ruleSetRule = TranslateRule(ruleSet);
|
||||
workflowRules.Add(ruleSetRule);
|
||||
|
||||
return new Workflow
|
||||
{
|
||||
RuleExpressionType = RulesEngine.Models.RuleExpressionType.LambdaExpression,
|
||||
Rules = workflowRules
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates a single rule to a Microsoft RulesEngine rule.
|
||||
/// </summary>
|
||||
/// <param name="rule">The rule to translate.</param>
|
||||
/// <returns>A Microsoft RulesEngine rule with the expression converted to a string.</returns>
|
||||
public RulesEngine.Models.Rule TranslateRule(IRule rule)
|
||||
{
|
||||
var expressionString = rule.Expression.Accept(_localRuleVisitor);
|
||||
return new RulesEngine.Models.Rule
|
||||
{
|
||||
RuleName = "R1",
|
||||
Expression = expressionString
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.Rules</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - Rules Engine</Product>
|
||||
<Description>Rules engine for Strata.SqlTools, providing rule-based validation and analysis of SQL queries and expressions.</Description>
|
||||
<PackageTags>sql;rules-engine;validation;analysis;query-validation</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with rules engine for SQL query validation and analysis.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Code Analysis -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RulesEngine" Version="5.0.3" PrivateAssets="compile;contentfiles;build;analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,191 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using SqlServerDeleteBreakdown = Strata.SqlTools.Breakdowns.SqlServer.DeleteBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a DELETE SQL statement breakdown with FROM and WHERE clauses for Snowflake.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class DeleteBreakdown : SqlServerDeleteBreakdown
|
||||
{
|
||||
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteBreakdown"/> class.
|
||||
/// </summary>
|
||||
public DeleteBreakdown() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fromClause">The FROM clause.</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public DeleteBreakdown(string fromClause, string whereClause, bool isMicrosoftSql = false)
|
||||
: base()
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
|
||||
|
||||
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
|
||||
FromClause.Clause = cleanFrom.Trim();
|
||||
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
|
||||
|
||||
var cleanWhere = parser.ExtractSqlComments(whereClause, out var whereComments);
|
||||
WhereClause.Clause = cleanWhere.Trim();
|
||||
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string for Snowflake.
|
||||
/// </summary>
|
||||
/// <returns>The DELETE SQL statement.</returns>
|
||||
protected override string GetSqlBreakdown()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Snowflake DELETE syntax is simpler - no DELETE clause with alias
|
||||
sb.AppendLine("DELETE FROM ");
|
||||
sb.AppendLine($" {FromClause.Clause}");
|
||||
|
||||
if (IsUsingWhereClause)
|
||||
{
|
||||
sb.AppendLine("WHERE ");
|
||||
sb.AppendLine($" {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Snowflake DELETE SQL statement into a DeleteBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
/// <returns>A DeleteBreakdown object representing the parsed statement.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static DeleteBreakdown Parse(string sql, bool isMicrosoftSql = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error, isMicrosoftSql))
|
||||
{
|
||||
throw new FormatException($"Failed to parse {(isMicrosoftSql ? "T-SQL" : "Snowflake SQL")} DELETE statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake DELETE SQL statement into a DeleteBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed DeleteBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out DeleteBreakdown result, bool isMicrosoftSql = false)
|
||||
=> TryParse(sql, out result, out _, isMicrosoftSql);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake DELETE SQL statement into a DeleteBreakdown object.
|
||||
/// Handles Snowflake-specific syntax.
|
||||
/// </summary>
|
||||
/// <param name="sql">The DELETE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed DeleteBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out DeleteBreakdown result, out string errorMessage, bool isMicrosoftSql = false)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// If Microsoft SQL mode, delegate to base class
|
||||
if (isMicrosoftSql)
|
||||
{
|
||||
if (!SqlServerDeleteBreakdown.TryParse(sql, out var baseResult, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert to Snowflake DeleteBreakdown
|
||||
result = new DeleteBreakdown
|
||||
{
|
||||
FromClause = baseResult.FromClause,
|
||||
WhereClause = baseResult.WhereClause,
|
||||
DeleteClause = baseResult.DeleteClause,
|
||||
SetupClauses = baseResult.SetupClauses,
|
||||
FinishClauses = baseResult.FinishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var parser = SnowflakeParserInstance;
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's a DELETE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
errorMessage = "SQL statement must start with DELETE.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract setup and finish clauses
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Snowflake uses simpler DELETE syntax: DELETE FROM table WHERE condition
|
||||
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
if (!deleteMatch.Success)
|
||||
{
|
||||
errorMessage = "Could not parse DELETE statement. Expected format: DELETE FROM table [WHERE condition]";
|
||||
return false;
|
||||
}
|
||||
|
||||
var fromClause = deleteMatch.Groups[1].Value.Trim();
|
||||
var whereClause = deleteMatch.Groups.Count > 2 ? deleteMatch.Groups[2].Value.Trim() : string.Empty;
|
||||
|
||||
result = new DeleteBreakdown(fromClause, whereClause, isMicrosoftSql: false)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using System.Collections;
|
||||
using Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
using SqlServerInsertBreakdown = Strata.SqlTools.Breakdowns.SqlServer.InsertBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an INSERT SQL statement breakdown with column and value clauses for Snowflake.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class InsertBreakdown : SqlServerInsertBreakdown
|
||||
{
|
||||
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class.
|
||||
/// </summary>
|
||||
public InsertBreakdown() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <param name="insertIntoClause">The column list for the INSERT.</param>
|
||||
/// <param name="valuesClause">The values clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public InsertBreakdown(string tableName, string insertIntoClause, string valuesClause, bool isMicrosoftSql = false)
|
||||
: base()
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
|
||||
|
||||
var cleanTable = parser.ExtractSqlComments(tableName, out var tableComments);
|
||||
TableName.Clause = cleanTable.Trim();
|
||||
TableName.Comment = tableComments.Count > 0 ? string.Join(" ", tableComments) : null;
|
||||
|
||||
var cleanInsert = parser.ExtractSqlComments(insertIntoClause, out var insertComments);
|
||||
InsertIntoClause.Clause = cleanInsert.Trim();
|
||||
InsertIntoClause.Comment = insertComments.Count > 0 ? string.Join(" ", insertComments) : null;
|
||||
|
||||
var cleanValues = parser.ExtractSqlComments(valuesClause, out var valuesComments);
|
||||
ValuesClause.Clause = cleanValues.Trim();
|
||||
ValuesClause.Comment = valuesComments.Count > 0 ? string.Join(" ", valuesComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InsertBreakdown"/> class from a list of column names.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <param name="columnNames">The list of column names.</param>
|
||||
public InsertBreakdown(string tableName, List<string> columnNames)
|
||||
: base()
|
||||
{
|
||||
TableName.Clause = tableName;
|
||||
InsertIntoClause.Clause = SqlUtils.GetSqlSafeColumnList(columnNames);
|
||||
|
||||
// Generate parameter names for values (Snowflake uses :parameter syntax)
|
||||
var valuesList = new List<string>();
|
||||
foreach (string item in columnNames)
|
||||
{
|
||||
valuesList.Add($":{item}");
|
||||
}
|
||||
ValuesClause.Clause = string.Join(",", valuesList);
|
||||
}
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Snowflake INSERT SQL statement into an InsertBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The INSERT SQL statement to parse.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
/// <returns>An InsertBreakdown object representing the parsed statement.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static InsertBreakdown Parse(string sql, bool isMicrosoftSql = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error, isMicrosoftSql))
|
||||
{
|
||||
throw new FormatException($"Failed to parse {(isMicrosoftSql ? "T-SQL" : "Snowflake SQL")} INSERT statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake INSERT SQL statement into an InsertBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The INSERT SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed InsertBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out InsertBreakdown result, bool isMicrosoftSql = false)
|
||||
=> TryParse(sql, out result, out _, isMicrosoftSql);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake INSERT SQL statement into an InsertBreakdown object.
|
||||
/// Handles Snowflake-specific syntax.
|
||||
/// </summary>
|
||||
/// <param name="sql">The INSERT SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed InsertBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out InsertBreakdown result, out string errorMessage, bool isMicrosoftSql = false)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// If Microsoft SQL mode, delegate to base class
|
||||
if (isMicrosoftSql)
|
||||
{
|
||||
if (!SqlServerInsertBreakdown.TryParse(sql, out var baseResult, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert to Snowflake InsertBreakdown
|
||||
result = new InsertBreakdown
|
||||
{
|
||||
TableName = baseResult.TableName,
|
||||
InsertIntoClause = baseResult.InsertIntoClause,
|
||||
ValuesClause = baseResult.ValuesClause,
|
||||
SetupClauses = baseResult.SetupClauses,
|
||||
FinishClauses = baseResult.FinishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var parser = SnowflakeParserInstance;
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's an INSERT statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
errorMessage = "SQL statement must start with INSERT INTO.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract setup and finish clauses
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Parse INSERT statement using regex
|
||||
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
if (!insertMatch.Success)
|
||||
{
|
||||
errorMessage = "Could not parse INSERT statement. Expected format: INSERT INTO table (columns) VALUES (values)";
|
||||
return false;
|
||||
}
|
||||
|
||||
var tableName = insertMatch.Groups[1].Value.Trim();
|
||||
var columnsClause = insertMatch.Groups[2].Value.Trim();
|
||||
var valuesClause = insertMatch.Groups[3].Value.Trim();
|
||||
|
||||
result = new InsertBreakdown(tableName, columnsClause, valuesClause, isMicrosoftSql: false)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using SqlServerProcedureBreakdown = Strata.SqlTools.Breakdowns.SqlServer.ProcedureBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Snowflake stored procedure call breakdown with procedure name and parameters.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ProcedureBreakdown : SqlServerProcedureBreakdown
|
||||
{
|
||||
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
|
||||
/// </summary>
|
||||
public ProcedureBreakdown() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="procedureName">The stored procedure name.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public ProcedureBreakdown(string procedureName, bool isMicrosoftSql = false) : base()
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
|
||||
|
||||
var cleanName = parser.ExtractSqlComments(procedureName, out var nameComments);
|
||||
ProcedureName.Clause = cleanName.Trim();
|
||||
ProcedureName.Comment = nameComments.Count > 0 ? string.Join(" ", nameComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcedureBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="procedureName">The stored procedure name.</param>
|
||||
/// <param name="parameters">The parameters dictionary (parameter name -> value expression).</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public ProcedureBreakdown(string procedureName, Dictionary<string, string> parameters, bool isMicrosoftSql = false)
|
||||
: this(procedureName, isMicrosoftSql)
|
||||
{
|
||||
Parameters = parameters ?? new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string for Snowflake.
|
||||
/// </summary>
|
||||
/// <returns>The CALL SQL statement (Snowflake uses CALL instead of EXEC).</returns>
|
||||
protected override string GetSqlBreakdown()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append("CALL ");
|
||||
sb.Append(ProcedureName.Clause);
|
||||
sb.Append("(");
|
||||
|
||||
if (IsUsingParameters)
|
||||
{
|
||||
var paramList = new List<string>();
|
||||
foreach (var param in Parameters)
|
||||
{
|
||||
// Snowflake uses positional or named parameters with => syntax
|
||||
paramList.Add($"{param.Key.TrimStart('@')} => {param.Value}");
|
||||
}
|
||||
sb.Append(string.Join(", ", paramList));
|
||||
}
|
||||
|
||||
sb.Append(")");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Snowflake CALL SQL statement into a ProcedureBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The CALL SQL statement to parse.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
/// <returns>A ProcedureBreakdown object representing the parsed statement.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static ProcedureBreakdown Parse(string sql, bool isMicrosoftSql = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error, isMicrosoftSql))
|
||||
{
|
||||
throw new FormatException($"Failed to parse {(isMicrosoftSql ? "EXEC" : "CALL")} statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake CALL SQL statement into a ProcedureBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The CALL SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out ProcedureBreakdown result, bool isMicrosoftSql = false)
|
||||
=> TryParse(sql, out result, out _, isMicrosoftSql);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake CALL SQL statement into a ProcedureBreakdown object.
|
||||
/// Handles Snowflake-specific syntax including CALL procedureName(param >= value).
|
||||
/// </summary>
|
||||
/// <param name="sql">The CALL SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed ProcedureBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out ProcedureBreakdown result, out string errorMessage, bool isMicrosoftSql = false)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// If Microsoft SQL mode, delegate to base class
|
||||
if (isMicrosoftSql)
|
||||
{
|
||||
if (!SqlServerProcedureBreakdown.TryParse(sql, out var baseResult, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert to Snowflake ProcedureBreakdown
|
||||
result = new ProcedureBreakdown
|
||||
{
|
||||
ProcedureName = baseResult.ProcedureName,
|
||||
Parameters = baseResult.Parameters,
|
||||
SetupClauses = baseResult.SetupClauses,
|
||||
FinishClauses = baseResult.FinishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var parser = SnowflakeParserInstance;
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's a CALL statement (Snowflake syntax) or EXEC (for compatibility)
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*(CALL|EXEC|EXECUTE)\b",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
errorMessage = "SQL statement must start with CALL, EXEC, or EXECUTE.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract setup and finish clauses
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Parse CALL statement - match procedure name and parameters
|
||||
// Pattern: CALL procedureName(param => value, ...)
|
||||
var callMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"(?:CALL|EXEC|EXECUTE)\s+([^\s\(]+)(?:\s*\((.*?)\))?$",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
if (!callMatch.Success)
|
||||
{
|
||||
errorMessage = "Could not parse CALL statement. Expected format: CALL procedureName(param => value, ...)";
|
||||
return false;
|
||||
}
|
||||
|
||||
var procedureName = callMatch.Groups[1].Value.Trim();
|
||||
var parametersText = callMatch.Groups.Count > 2 ? callMatch.Groups[2].Value.Trim() : string.Empty;
|
||||
|
||||
var parameters = new Dictionary<string, string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parametersText))
|
||||
{
|
||||
// Parse parameters - Snowflake uses param => value syntax
|
||||
var paramMatches = System.Text.RegularExpressions.Regex.Matches(parametersText,
|
||||
@"(\w+)\s*=>\s*([^,]+)(?:,|$)",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (System.Text.RegularExpressions.Match paramMatch in paramMatches)
|
||||
{
|
||||
var paramName = paramMatch.Groups[1].Value.Trim();
|
||||
var paramValue = paramMatch.Groups[2].Value.Trim();
|
||||
// Store with @ prefix for consistency with SQL Server
|
||||
parameters["@" + paramName] = paramValue;
|
||||
}
|
||||
|
||||
// If no named parameters found, try positional parameters (just values)
|
||||
if (parameters.Count == 0 && !string.IsNullOrWhiteSpace(parametersText))
|
||||
{
|
||||
var positionalParams = parametersText.Split(',');
|
||||
for (int i = 0; i < positionalParams.Length; i++)
|
||||
{
|
||||
parameters[$"@param{i + 1}"] = positionalParams[i].Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = new ProcedureBreakdown(procedureName, parameters, isMicrosoftSql: false)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,881 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
using CommandVisitor = Strata.SqlTools.Visitors.Snowflake.CommandVisitor;
|
||||
using SqlClause = Strata.SqlTools.SqlBreakdown.Classes.SqlClause;
|
||||
using SqlExpressionClause = Strata.SqlTools.SqlBreakdown.Classes.SqlExpressionClause;
|
||||
using SqlServerCommandVisitor = Strata.SqlTools.Visitors.SqlServer.CommandVisitor;
|
||||
using SqlServerQueryBreakdown = Strata.SqlTools.Breakdowns.SqlServer.QueryBreakdown;
|
||||
using StatementExpressionParser = Strata.SqlTools.Statements.Snowflake.StatementExpressionParser;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Snowflake SQL query breakdown with all clauses, following Snowflake SQL standards.
|
||||
/// Handles both :parameter and @parameter syntax for Snowflake compatibility.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class QueryBreakdown : SqlServerQueryBreakdown
|
||||
{
|
||||
private const string ExpressionNullErrorMessage = "Expression cannot be null.";
|
||||
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class.
|
||||
/// </summary>
|
||||
public QueryBreakdown() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT and FROM clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public QueryBreakdown(string selectClause, string fromClause, bool isMicrosoftSql = false) : base()
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
|
||||
|
||||
var cleanSelect = parser.ExtractSqlComments(selectClause, out var selectComments);
|
||||
SelectClause.Clause = cleanSelect.Trim();
|
||||
SelectClause.Comment = selectComments.Count > 0 ? string.Join(" ", selectComments) : null;
|
||||
|
||||
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
|
||||
FromClause.Clause = cleanFrom.Trim();
|
||||
FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT, FROM, and WHERE clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause.</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public QueryBreakdown(string selectClause, string fromClause, string whereClause, bool isMicrosoftSql = false)
|
||||
: this(selectClause, fromClause, isMicrosoftSql)
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
|
||||
|
||||
var cleanWhere = parser.ExtractSqlComments(whereClause, out var whereComments);
|
||||
WhereClause.Clause = cleanWhere.Trim();
|
||||
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdown"/> class with SELECT, FROM, WHERE, and ORDER BY clauses.
|
||||
/// </summary>
|
||||
/// <param name="selectClause">The SELECT clause.</param>
|
||||
/// <param name="fromClause">The FROM clause.</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
/// <param name="orderByClause">The ORDER BY clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public QueryBreakdown(string selectClause, string fromClause, string whereClause, string orderByClause, bool isMicrosoftSql = false)
|
||||
: this(selectClause, fromClause, whereClause, isMicrosoftSql)
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
|
||||
|
||||
var cleanOrderBy = parser.ExtractSqlComments(orderByClause, out var orderByComments);
|
||||
OrderByClause.Clause = cleanOrderBy.Trim();
|
||||
OrderByClause.Comment = orderByComments.Count > 0 ? string.Join(" ", orderByComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a parameter to the query using Snowflake's :param format.
|
||||
/// Also adds @param format for compatibility.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name (with or without : or @).</param>
|
||||
/// <param name="value">The parameter value.</param>
|
||||
public new void AddParameter(string parameterName, object value)
|
||||
{
|
||||
// Convert to Snowflake format (: prefix)
|
||||
var colonName = NormalizeParameterName(parameterName);
|
||||
var atName = "@" + colonName.TrimStart(':', '@');
|
||||
|
||||
// Use base class internal list
|
||||
base.AddParameter(colonName.TrimStart(':', '@'), value);
|
||||
|
||||
// Add both formats to dictionary for compatibility
|
||||
if (Parameters.ContainsKey($"@{colonName.TrimStart(':', '@')}"))
|
||||
{
|
||||
Parameters.Remove($"@{colonName.TrimStart(':', '@')}");
|
||||
}
|
||||
Parameters[colonName] = value;
|
||||
Parameters[atName] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a parameter using Snowflake's :param format.
|
||||
/// Also updates @param format for compatibility.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name (with or without : or @).</param>
|
||||
/// <param name="value">The parameter value.</param>
|
||||
public new void SetParameterValue(string parameterName, object value)
|
||||
{
|
||||
var colonName = NormalizeParameterName(parameterName);
|
||||
var atName = "@" + colonName.TrimStart(':', '@');
|
||||
|
||||
Parameters[colonName] = value;
|
||||
Parameters[atName] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes parameter name to Snowflake format (:param).
|
||||
/// </summary>
|
||||
private static string NormalizeParameterName(string parameterName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(parameterName))
|
||||
{
|
||||
return parameterName;
|
||||
}
|
||||
|
||||
// If it already has : or @, preserve the prefix but prefer :
|
||||
if (parameterName.StartsWith(':'))
|
||||
{
|
||||
return parameterName;
|
||||
}
|
||||
|
||||
if (parameterName.StartsWith('@'))
|
||||
{
|
||||
return ":" + parameterName.Substring(1);
|
||||
}
|
||||
|
||||
// Add : prefix
|
||||
return ":" + parameterName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures parameters exist in both @ and : formats for compatibility.
|
||||
/// </summary>
|
||||
private void NormalizeParameterFormats()
|
||||
{
|
||||
var paramKeys = Parameters.Keys.ToList();
|
||||
foreach (var paramName in paramKeys)
|
||||
{
|
||||
if (paramName.StartsWith(':'))
|
||||
{
|
||||
// Add @param version
|
||||
var atParam = "@" + paramName.Substring(1);
|
||||
if (!Parameters.ContainsKey(atParam))
|
||||
{
|
||||
Parameters[atParam] = Parameters[paramName];
|
||||
}
|
||||
}
|
||||
else if (paramName.StartsWith('@'))
|
||||
{
|
||||
// Add :param version
|
||||
var colonParam = ":" + paramName.Substring(1);
|
||||
if (!Parameters.ContainsKey(colonParam))
|
||||
{
|
||||
Parameters[colonParam] = Parameters[paramName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the SELECT clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
|
||||
public void AddSelectExpression(Expression expression, string? comment = null, bool isMicrosoftSql = false)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
||||
}
|
||||
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SelectClause.Clause))
|
||||
{
|
||||
SelectClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectClause.Clause = $"{SelectClause.Clause}, {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SelectClause.Comment))
|
||||
{
|
||||
SelectClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectClause.Comment = $"{SelectClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the WHERE clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
/// <param name="operation">The logical operation ("and" or "or"). Defaults to "and".</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
|
||||
public void AddWhereExpression(Expression expression, string? comment = null, string operation = "and", bool isMicrosoftSql = false)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
||||
}
|
||||
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
||||
{
|
||||
WhereClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
||||
{
|
||||
WhereClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Comment = $"{WhereClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WHERE clause condition. Defaults to "and" operation.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL condition to add.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public void AddWhereClause(string sql, bool isMicrosoftSql = false)
|
||||
{
|
||||
AddWhereClause(sql, "and", isMicrosoftSql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WHERE clause condition with a specific logical operation.
|
||||
/// Extracts and preserves any SQL comments in the clause.
|
||||
/// Uses Snowflake parsing rules by default.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL condition to add.</param>
|
||||
/// <param name="operation">The logical operation ("and" or "or").</param>
|
||||
public override void AddWhereClause(string sql, string operation)
|
||||
{
|
||||
AddWhereClause(sql, operation, isMicrosoftSql: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WHERE clause condition with a specific logical operation.
|
||||
/// Extracts and preserves any SQL comments in the clause.
|
||||
/// Automatically extracts parameters from the WHERE clause and adds them to the Parameters dictionary.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL condition to add.</param>
|
||||
/// <param name="operation">The logical operation ("and" or "or").</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public void AddWhereClause(string sql, string operation, bool isMicrosoftSql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use appropriate parser based on SQL dialect
|
||||
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
|
||||
|
||||
// Extract comments from the incoming SQL
|
||||
var cleanSql = parser.ExtractSqlComments(sql, out var comments);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Clause))
|
||||
{
|
||||
WhereClause.Clause = cleanSql.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Clause = $"{WhereClause.Clause} {operation} {cleanSql.Trim()}";
|
||||
}
|
||||
|
||||
// Merge comments
|
||||
if (comments.Count > 0)
|
||||
{
|
||||
var newComment = string.Join(" ", comments);
|
||||
if (string.IsNullOrWhiteSpace(WhereClause.Comment))
|
||||
{
|
||||
WhereClause.Comment = newComment;
|
||||
}
|
||||
else
|
||||
{
|
||||
WhereClause.Comment = $"{WhereClause.Comment} {newComment}";
|
||||
}
|
||||
}
|
||||
|
||||
// Extract and add parameters from the WHERE clause using appropriate parser
|
||||
ExtractAndAddParametersWithParser(cleanSql, parser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts parameters from a SQL clause and adds them to the Parameters dictionary using the specified parser.
|
||||
/// </summary>
|
||||
/// <param name="sql">The SQL clause to extract parameters from.</param>
|
||||
/// <param name="parser">The parser to use for extracting parameters.</param>
|
||||
private void ExtractAndAddParametersWithParser(string sql, Statements.SqlServer.StatementParser parser)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a temporary dictionary to extract parameters
|
||||
var tempParams = new Dictionary<string, object>();
|
||||
parser.ExtractParameters(tempParams, sql);
|
||||
|
||||
// Add each parameter using the managed add method from base class
|
||||
foreach (var kvp in tempParams)
|
||||
{
|
||||
AddOrUpdateParameter(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the GROUP BY clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
public override void AddGroupByExpression(Expression expression, string? comment = null)
|
||||
{
|
||||
AddGroupByExpression(expression, comment, isMicrosoftSql: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the GROUP BY clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
|
||||
public void AddGroupByExpression(Expression expression, string? comment, bool isMicrosoftSql)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
||||
}
|
||||
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(GroupByClause.Clause))
|
||||
{
|
||||
GroupByClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
GroupByClause.Clause = $"{GroupByClause.Clause}, {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(GroupByClause.Comment))
|
||||
{
|
||||
GroupByClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
GroupByClause.Comment = $"{GroupByClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the ORDER BY clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
public override void AddOrderByExpression(Expression expression, string? comment = null)
|
||||
{
|
||||
AddOrderByExpression(expression, comment, isMicrosoftSql: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the ORDER BY clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
|
||||
public void AddOrderByExpression(Expression expression, string? comment, bool isMicrosoftSql)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
||||
}
|
||||
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(OrderByClause.Clause))
|
||||
{
|
||||
OrderByClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
OrderByClause.Clause = $"{OrderByClause.Clause}, {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(OrderByClause.Comment))
|
||||
{
|
||||
OrderByClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
OrderByClause.Comment = $"{OrderByClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the HAVING clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
/// <param name="operation">The logical operation ("and" or "or"). Defaults to "and".</param>
|
||||
public override void AddHavingExpression(Expression expression, string? comment = null, string operation = "and")
|
||||
{
|
||||
AddHavingExpression(expression, comment, operation, isMicrosoftSql: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an expression to the HAVING clause.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to add.</param>
|
||||
/// <param name="comment">Optional comment to add with the expression.</param>
|
||||
/// <param name="operation">The logical operation ("and" or "or"). Defaults to "and".</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL formatting. If false, uses Snowflake formatting. Defaults to false.</param>
|
||||
public void AddHavingExpression(Expression expression, string? comment, string operation, bool isMicrosoftSql)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression), ExpressionNullErrorMessage);
|
||||
}
|
||||
|
||||
var visitor = isMicrosoftSql
|
||||
? (IVisitor<string>)new SqlServerCommandVisitor()
|
||||
: new CommandVisitor();
|
||||
var sql = expression.Accept(visitor);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(HavingClause.Clause))
|
||||
{
|
||||
HavingClause.Clause = sql;
|
||||
}
|
||||
else
|
||||
{
|
||||
HavingClause.Clause = $"{HavingClause.Clause} {operation} {sql}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(comment))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(HavingClause.Comment))
|
||||
{
|
||||
HavingClause.Comment = comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
HavingClause.Comment = $"{HavingClause.Comment} {comment}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete Snowflake SQL query string with proper formatting.
|
||||
/// </summary>
|
||||
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
|
||||
/// <returns>The Snowflake SQL query string.</returns>
|
||||
#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high
|
||||
public override string GetSql(bool includeSetupFinish = true)
|
||||
#pragma warning restore S3776
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (includeSetupFinish)
|
||||
{
|
||||
foreach (string setup in SetupClauses)
|
||||
{
|
||||
sb.AppendLine(setup);
|
||||
}
|
||||
}
|
||||
|
||||
if (IsUsingWithClause)
|
||||
{
|
||||
// Check if any WITH clause is recursive
|
||||
bool hasRecursive = WithClauses.Any(wc => wc.IsRecursive);
|
||||
sb.Append("WITH");
|
||||
if (hasRecursive)
|
||||
{
|
||||
sb.Append(" RECURSIVE");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
for (int i = 0; i < WithClauses.Count; i++)
|
||||
{
|
||||
var withClause = WithClauses[i];
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Include comment if present
|
||||
if (!string.IsNullOrWhiteSpace(withClause.Comment))
|
||||
{
|
||||
sb.AppendLine($" {withClause.Comment}");
|
||||
}
|
||||
|
||||
// Write CTE name with optional column list
|
||||
var cteName = withClause.TableName;
|
||||
if (withClause.ColumnList != null && withClause.ColumnList.Count > 0)
|
||||
{
|
||||
var columnList = string.Join(", ", withClause.ColumnList);
|
||||
cteName = $"{withClause.TableName} ({columnList})";
|
||||
}
|
||||
|
||||
sb.AppendLine($" {cteName} AS (");
|
||||
|
||||
if (withClause.IsRecursive && withClause.RecursiveQuery != null)
|
||||
{
|
||||
// For recursive CTEs: anchor query UNION ALL recursive query
|
||||
var anchorSql = withClause.Query?.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
|
||||
var recursiveSql = withClause.RecursiveQuery.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
|
||||
sb.AppendLine($" {anchorSql}");
|
||||
sb.AppendLine(" UNION ALL");
|
||||
sb.AppendLine($" {recursiveSql}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-recursive CTEs: just the single query
|
||||
var withSql = withClause.Query?.GetSql(includeSetupFinish: false).Trim() ?? string.Empty;
|
||||
sb.AppendLine($" {withSql}");
|
||||
}
|
||||
sb.Append(" )");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Snowflake SELECT syntax
|
||||
sb.Append(StatementParser.KeywordSelect);
|
||||
|
||||
// Handle TOP equivalent using LIMIT in Snowflake
|
||||
sb.AppendLine();
|
||||
if (!string.IsNullOrEmpty(SelectClause.Comment))
|
||||
{
|
||||
sb.AppendLine($" {SelectClause.Comment}");
|
||||
}
|
||||
sb.AppendLine($" {SelectClause.Clause}");
|
||||
|
||||
if (IsUsingFromClause)
|
||||
{
|
||||
sb.AppendLine(StatementParser.KeywordFrom);
|
||||
if (!string.IsNullOrEmpty(FromClause.Comment))
|
||||
{
|
||||
sb.AppendLine($" {FromClause.Comment}");
|
||||
}
|
||||
sb.AppendLine($" {FromClause.Clause}");
|
||||
}
|
||||
|
||||
if (IsUsingWhereClause)
|
||||
{
|
||||
sb.AppendLine(StatementParser.KeywordWhere);
|
||||
if (!string.IsNullOrEmpty(WhereClause.Comment))
|
||||
{
|
||||
sb.AppendLine($" {WhereClause.Comment}");
|
||||
}
|
||||
sb.AppendLine($" {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
if (IsUsingGroupByClause)
|
||||
{
|
||||
sb.AppendLine(StatementParser.KeywordGroupBy);
|
||||
if (!string.IsNullOrEmpty(GroupByClause.Comment))
|
||||
{
|
||||
sb.AppendLine($" {GroupByClause.Comment}");
|
||||
}
|
||||
sb.AppendLine($" {GroupByClause.Clause}");
|
||||
}
|
||||
|
||||
if (IsUsingHavingClause)
|
||||
{
|
||||
sb.AppendLine(StatementParser.KeywordHaving);
|
||||
if (!string.IsNullOrEmpty(HavingClause.Comment))
|
||||
{
|
||||
sb.AppendLine($" {HavingClause.Comment}");
|
||||
}
|
||||
sb.AppendLine($" {HavingClause.Clause}");
|
||||
}
|
||||
|
||||
if (IsUsingOrderByClause)
|
||||
{
|
||||
sb.AppendLine(StatementParser.KeywordOrderBy);
|
||||
if (!string.IsNullOrEmpty(OrderByClause.Comment))
|
||||
{
|
||||
sb.AppendLine($" {OrderByClause.Comment}");
|
||||
}
|
||||
sb.AppendLine($" {OrderByClause.Clause}");
|
||||
}
|
||||
|
||||
if (includeSetupFinish)
|
||||
{
|
||||
foreach (string finish in FinishClauses)
|
||||
{
|
||||
sb.AppendLine(finish);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep clone of this Snowflake query breakdown.
|
||||
/// </summary>
|
||||
/// <returns>A cloned SnowflakeQueryBreakdown instance.</returns>
|
||||
public new object Clone()
|
||||
{
|
||||
// Use the base class clone method but return as SnowflakeQueryBreakdown
|
||||
var baseClone = (SqlServerQueryBreakdown)base.Clone();
|
||||
|
||||
var clone = new QueryBreakdown
|
||||
{
|
||||
SelectClause = baseClone.SelectClause,
|
||||
FromClause = baseClone.FromClause,
|
||||
WhereClause = baseClone.WhereClause,
|
||||
GroupByClause = baseClone.GroupByClause,
|
||||
HavingClause = baseClone.HavingClause,
|
||||
OrderByClause = baseClone.OrderByClause,
|
||||
SetupClauses = new List<string>(baseClone.SetupClauses),
|
||||
FinishClauses = new ArrayList(baseClone.FinishClauses)
|
||||
};
|
||||
|
||||
// Copy parameters
|
||||
foreach (var kvp in baseClone.Parameters)
|
||||
{
|
||||
clone.Parameters[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a Common Table Expression (CTE) to the WITH clause using a raw SQL string.
|
||||
/// Parses the SQL using Snowflake SQL rules.
|
||||
/// </summary>
|
||||
/// <param name="withTableName">The table name for the WITH clause.</param>
|
||||
/// <param name="withTableSql">The SQL query for the WITH table.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to true.</param>
|
||||
public override void AddWithClause(string withTableName, string withTableSql, bool isMicrosoftSql = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(withTableName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(withTableName), "WITH table name cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(withTableSql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(withTableSql), "WITH table SQL cannot be null or empty.");
|
||||
}
|
||||
|
||||
// Parse the SQL string into a SnowflakeQueryBreakdown object using specified parsing rules
|
||||
var parsedQuery = QueryBreakdown.Parse(withTableSql, isMicrosoftSql);
|
||||
|
||||
// Delegate to the IQueryBreakdown overload
|
||||
AddWithClause(withTableName, parsedQuery);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Snowflake-specific statement expression parser.
|
||||
/// </summary>
|
||||
/// <returns>A Snowflake IStatementExpressionParser instance.</returns>
|
||||
protected override IStatementExpressionParser CreateExpressionParser()
|
||||
=> new StatementExpressionParser();
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object.
|
||||
/// Supports both :parameter and @parameter syntax.
|
||||
/// </summary>
|
||||
/// <param name="sql">The Snowflake SQL SELECT statement to parse.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>A SnowflakeQueryBreakdown object representing the parsed query.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static QueryBreakdown Parse(string sql, bool isMicrosoftSql = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error, isMicrosoftSql))
|
||||
{
|
||||
throw new FormatException($"Failed to parse {(isMicrosoftSql ? "T-SQL" : "Snowflake SQL")} statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The Snowflake SQL SELECT statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed SnowflakeQueryBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out QueryBreakdown result, bool isMicrosoftSql = false)
|
||||
=> TryParse(sql, out result, out _, isMicrosoftSql);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake SQL SELECT statement into a SnowflakeQueryBreakdown object.
|
||||
/// Handles Snowflake-specific syntax including :parameter and @parameter formats.
|
||||
/// </summary>
|
||||
/// <param name="sql">The Snowflake SQL SELECT statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed SnowflakeQueryBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out QueryBreakdown result, out string errorMessage, bool isMicrosoftSql = false)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// If Microsoft SQL mode, delegate to base class
|
||||
if (isMicrosoftSql)
|
||||
{
|
||||
if (!SqlServerQueryBreakdown.TryParse(sql, out var baseResult, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert base QueryBreakdown to SnowflakeQueryBreakdown
|
||||
result = new QueryBreakdown
|
||||
{
|
||||
SelectClause = baseResult.SelectClause,
|
||||
FromClause = baseResult.FromClause,
|
||||
WhereClause = baseResult.WhereClause,
|
||||
GroupByClause = baseResult.GroupByClause,
|
||||
HavingClause = baseResult.HavingClause,
|
||||
OrderByClause = baseResult.OrderByClause,
|
||||
SetupClauses = baseResult.SetupClauses,
|
||||
FinishClauses = baseResult.FinishClauses
|
||||
};
|
||||
|
||||
// Preserve WITH clause using protected helper
|
||||
result.SetWithClauseValue(baseResult.GetWithClauseValue());
|
||||
|
||||
// Copy parameters
|
||||
foreach (var param in baseResult.Parameters)
|
||||
{
|
||||
result.Parameters[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normalize the SQL: remove extra whitespace, handle line breaks, preserve comments
|
||||
sql = SnowflakeParserInstance.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Extract setup clauses (everything before the main SELECT)
|
||||
var setupClauses = new List<string>();
|
||||
sql = SnowflakeParserInstance.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
// Extract finish clauses (cleanup statements after the main query)
|
||||
var finishClauses = new ArrayList();
|
||||
sql = SnowflakeParserInstance.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Parse WITH clause separately if present
|
||||
string? withClause = null;
|
||||
if (SnowflakeParserInstance.TryParseWithClause(sql, out withClause, out var mainQuery))
|
||||
{
|
||||
sql = mainQuery; // Continue parsing with the main query
|
||||
}
|
||||
|
||||
// Parse the main SELECT statement
|
||||
if (!SnowflakeParserInstance.TryParseSelectStatement(sql, out var clauses, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the SnowflakeQueryBreakdown object
|
||||
result = new QueryBreakdown
|
||||
{
|
||||
SelectClause = clauses!.SelectClause ?? new SqlExpressionClause(splitOnComma: true),
|
||||
FromClause = clauses.FromClause ?? new SqlClause(),
|
||||
WhereClause = clauses.WhereClause ?? new SqlExpressionClause(splitOnComma: false),
|
||||
GroupByClause = clauses.GroupByClause ?? new SqlExpressionClause(splitOnComma: true),
|
||||
HavingClause = clauses.HavingClause ?? new SqlExpressionClause(splitOnComma: false),
|
||||
OrderByClause = clauses.OrderByClause ?? new SqlExpressionClause(splitOnComma: true),
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
// Preserve WITH clause
|
||||
result.SetWithClauseValue(withClause?.Trim());
|
||||
|
||||
// Extract parameters from all clauses (use comment-free version for this)
|
||||
var sqlWithoutComments = SnowflakeParserInstance.RemoveSqlComments(sql);
|
||||
SnowflakeParserInstance.ExtractParameters(result.Parameters, sqlWithoutComments);
|
||||
|
||||
// Normalize parameters to include both @ and : formats for compatibility
|
||||
result.NormalizeParameterFormats();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during Snowflake SQL parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a LINQ to SQL query of the specified type based on this breakdown.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type for the query.</typeparam>
|
||||
/// <returns>null by default, as QueryBreakdown operates on SQL. Override in derived classes to provide LINQ query reconstruction.</returns>
|
||||
/// <remarks>
|
||||
/// This Snowflake-specific implementation returns null since Snowflake QueryBreakdown represents parsed SQL statements.
|
||||
/// Derived classes can override this method to reconstruct LINQ queries from the analyzed components.
|
||||
/// </remarks>
|
||||
public override IQueryable<T>? GetQuery<T>() where T : class
|
||||
{
|
||||
// Snowflake breakdown represents parsed SQL statements and does not have a built-in way to create LINQ queries
|
||||
// Override in derived classes to provide LINQ query reconstruction if needed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,940 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Snowflake SQL-specific collection for managing multiple QueryBreakdown objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class extends SqlBreakdownCollection with Snowflake-specific functionality,
|
||||
/// including support for Snowflake features like semi-structured data, stage references,
|
||||
/// time travel, snowflake-specific parameters (:parameter and @parameter syntax),
|
||||
/// and proper batch handling.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class QueryBreakdownCollection : SqlBreakdownCollection
|
||||
{
|
||||
private readonly List<QueryBreakdown> _queryBreakdowns;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class for Snowflake.
|
||||
/// </summary>
|
||||
public QueryBreakdownCollection() : base()
|
||||
{
|
||||
_queryBreakdowns = new List<QueryBreakdown>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryBreakdownCollection"/> class with initial query breakdowns.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdowns">The initial collection of query breakdowns.</param>
|
||||
public QueryBreakdownCollection(IEnumerable<QueryBreakdown> queryBreakdowns)
|
||||
: base(queryBreakdowns?.Cast<ISqlBreakdown>() ?? Enumerable.Empty<ISqlBreakdown>())
|
||||
{
|
||||
_queryBreakdowns = new List<QueryBreakdown>(queryBreakdowns ?? Enumerable.Empty<QueryBreakdown>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of QueryBreakdown objects.
|
||||
/// </summary>
|
||||
public IReadOnlyList<QueryBreakdown> QueryBreakdowns => _queryBreakdowns.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a QueryBreakdown to the collection.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The query breakdown to add.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when queryBreakdown is null.</exception>
|
||||
public void Add(QueryBreakdown queryBreakdown)
|
||||
{
|
||||
if (queryBreakdown == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(queryBreakdown));
|
||||
}
|
||||
|
||||
_queryBreakdowns.Add(queryBreakdown);
|
||||
base.Add(queryBreakdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple QueryBreakdowns to the collection.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdowns">The query breakdowns to add.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when queryBreakdowns is null.</exception>
|
||||
public void AddRange(IEnumerable<QueryBreakdown> queryBreakdowns)
|
||||
{
|
||||
if (queryBreakdowns == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(queryBreakdowns));
|
||||
}
|
||||
|
||||
foreach (var breakdown in queryBreakdowns)
|
||||
{
|
||||
Add(breakdown);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a QueryBreakdown from the collection.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The query breakdown to remove.</param>
|
||||
/// <returns>True if removed; otherwise, false.</returns>
|
||||
public bool Remove(QueryBreakdown queryBreakdown)
|
||||
{
|
||||
var removed = _queryBreakdowns.Remove(queryBreakdown);
|
||||
if (removed)
|
||||
{
|
||||
base.Remove(queryBreakdown);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all query breakdowns from the collection.
|
||||
/// </summary>
|
||||
public new void Clear()
|
||||
{
|
||||
_queryBreakdowns.Clear();
|
||||
base.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Snowflake SQL batch representation with Snowflake-specific formatting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Generates Snowflake SQL with proper statement separation and optional session setup.
|
||||
/// Snowflake uses semicolons as statement separators instead of GO.
|
||||
/// </remarks>
|
||||
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
|
||||
/// <param name="includeSessionSetup">Whether to include session context setup statements.</param>
|
||||
/// <returns>The formatted Snowflake SQL batch.</returns>
|
||||
public string GetSnowflakeBatch(bool includeSetupFinish = true, bool includeSessionSetup = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Add session setup if requested
|
||||
if (includeSessionSetup)
|
||||
{
|
||||
sb.AppendLine("-- Snowflake Session Setup");
|
||||
sb.AppendLine("ALTER SESSION SET NULLABLE_AS_NULL = FALSE;");
|
||||
sb.AppendLine("ALTER SESSION SET ERROR_ON_NONDETERMINISTIC_UPDATE = FALSE;");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Add all queries with semicolon separators
|
||||
if (_queryBreakdowns.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < _queryBreakdowns.Count; i++)
|
||||
{
|
||||
var query = _queryBreakdowns[i];
|
||||
var sql = query.GetSql(includeSetupFinish);
|
||||
|
||||
// Ensure proper termination
|
||||
var trimmed = sql.TrimEnd();
|
||||
sb.Append(trimmed);
|
||||
|
||||
if (!trimmed.EndsWith(';'))
|
||||
{
|
||||
sb.Append(";");
|
||||
}
|
||||
|
||||
// Add spacing between statements
|
||||
if (i < _queryBreakdowns.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that reference Snowflake stages (using @ or @~ syntax).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Stage references use the pattern @stage_name/ or @~/stage_name/.
|
||||
/// This specifically matches stage references and avoids false positives from @parameter syntax.
|
||||
/// </remarks>
|
||||
/// <param name="stageName">Optional stage name to filter by. If null, returns all queries using any stage.</param>
|
||||
/// <returns>Query breakdowns that reference stages.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereUseStageReference(string? stageName = null)
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
{
|
||||
var sql = q.GetSql();
|
||||
|
||||
// Use regex to match stage references: @stage_name/ or @~/stage_name/
|
||||
// This avoids false positives from @parameter syntax
|
||||
var stagePattern = @"@[\w~]+/";
|
||||
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sql, stagePattern))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stageName == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var specificPattern = stageName.Contains("~")
|
||||
? $@"@~/{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@', '~', '/'))}/"
|
||||
: $@"@{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@'))}/";
|
||||
|
||||
return System.Text.RegularExpressions.Regex.IsMatch(sql, specificPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that reference JSON/semi-structured data using Snowflake's JSON operators.
|
||||
/// </summary>
|
||||
/// <returns>Query breakdowns that use JSON functions or colon notation.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereUseSemiStructuredData()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
{
|
||||
var sql = q.GetSql().ToUpperInvariant();
|
||||
|
||||
// Check for JSON functions or colon notation used in semi-structured data
|
||||
return sql.Contains("JSON_") ||
|
||||
sql.Contains("OBJECT_") ||
|
||||
sql.Contains("ARRAY_") ||
|
||||
sql.Contains("FLATTEN(") ||
|
||||
sql.Contains(":VALUE") ||
|
||||
sql.Contains(":NAME") ||
|
||||
sql.Contains(":TYPE");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that use Snowflake-specific parameter syntax (:param or @param).
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The parameter name (with or without : or @).</param>
|
||||
/// <returns>Query breakdowns using the specified Snowflake parameter.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereUseSnowflakeParameter(string parameterName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parameterName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(parameterName));
|
||||
}
|
||||
|
||||
// Normalize parameter name (remove : or @)
|
||||
var cleanName = parameterName.TrimStart(':', '@');
|
||||
|
||||
return _queryBreakdowns.Where(q =>
|
||||
{
|
||||
var sql = q.GetSql();
|
||||
return sql.Contains($":{cleanName}", StringComparison.OrdinalIgnoreCase) ||
|
||||
sql.Contains($"@{cleanName}", StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that use Snowflake time travel features.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Detects use of BEFORE, AT, or MATCH_CONDITION clauses for time travel queries.
|
||||
/// </remarks>
|
||||
/// <returns>Query breakdowns using time travel syntax.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereUseTimeTravelFeature()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
{
|
||||
var sql = q.GetSql().ToUpperInvariant();
|
||||
|
||||
return sql.Contains("BEFORE (") ||
|
||||
sql.Contains("AT (") ||
|
||||
sql.Contains("MATCH_CONDITION");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that use Snowflake functions (PARSE_JSON, OBJECT_INSERT, ARRAY, etc.).
|
||||
/// </summary>
|
||||
/// <returns>Query breakdowns using Snowflake-specific functions.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereUseSnowflakeFunctions()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
{
|
||||
var sql = q.GetSql().ToUpperInvariant();
|
||||
|
||||
var snowflakeFunctions = new[]
|
||||
{
|
||||
"PARSE_JSON", "OBJECT_INSERT", "ARRAY_CONSTRUCT", "ARRAY_AGG",
|
||||
"FLATTEN", "GET_PATH", "TRY_PARSE_JSON", "JSON_EXTRACT_PATH_TEXT",
|
||||
"JSON_EXTRACT_PATH_WITH_DEFAULT", "HASHAGGREGATE", "LISTAGG",
|
||||
"APPROX_COUNT_DISTINCT", "APPROX_PERCENTILE", "GREATEST", "LEAST",
|
||||
"NULLIF", "ZEROIFNULL", "STRTOK", "SPLIT_PART", "PIVOT", "UNPIVOT"
|
||||
};
|
||||
|
||||
return snowflakeFunctions.Any(func => sql.Contains(func));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that reference temporary or dynamic tables.
|
||||
/// </summary>
|
||||
/// <returns>Query breakdowns using temporary tables.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereUseTemporaryTables()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
{
|
||||
var sql = q.GetSql().ToUpperInvariant();
|
||||
|
||||
return sql.Contains("TEMPORARY TABLE") ||
|
||||
sql.Contains("TEMP TABLE") ||
|
||||
sql.Contains("CREATE TEMP ") ||
|
||||
sql.Contains("DYNAMIC TABLE");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that reference external tables or stages.
|
||||
/// </summary>
|
||||
/// <returns>Query breakdowns using external data sources.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereUseExternalData()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
{
|
||||
var sql = q.GetSql().ToUpperInvariant();
|
||||
|
||||
return sql.Contains("EXTERNAL TABLE") ||
|
||||
sql.Contains(" FROM @") ||
|
||||
sql.Contains("COPY INTO @");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries by the SELECT clause content using Snowflake's format.
|
||||
/// </summary>
|
||||
/// <param name="selectContains">The text to find in the SELECT clause.</param>
|
||||
/// <returns>Filtered query breakdowns.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereSelectContains(string selectContains)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(selectContains))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selectContains));
|
||||
}
|
||||
|
||||
return _queryBreakdowns.Where(q =>
|
||||
q.SelectClause?.Clause?.Contains(selectContains, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that reference specific tables or schemas.
|
||||
/// </summary>
|
||||
/// <param name="tableNameContains">The table name or schema pattern to find.</param>
|
||||
/// <returns>Filtered query breakdowns.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereTableContains(string tableNameContains)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tableNameContains))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tableNameContains));
|
||||
}
|
||||
|
||||
return _queryBreakdowns.Where(q =>
|
||||
q.FromClause?.Clause?.Contains(tableNameContains, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that have WHERE clauses.
|
||||
/// </summary>
|
||||
/// <returns>Query breakdowns with WHERE clauses.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereHaveWhereClause()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
!string.IsNullOrWhiteSpace(q.WhereClause?.Clause));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries without WHERE clauses (potentially risky for full table scans).
|
||||
/// </summary>
|
||||
/// <returns>Query breakdowns without WHERE clauses.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereHaveNoWhereClause()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
string.IsNullOrWhiteSpace(q.WhereClause?.Clause));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that have GROUP BY clauses.
|
||||
/// </summary>
|
||||
/// <returns>Query breakdowns with GROUP BY clauses.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereHaveGroupByClause()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
!string.IsNullOrWhiteSpace(q.GroupByClause?.Clause));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters queries that have ORDER BY clauses.
|
||||
/// </summary>
|
||||
/// <returns>Query breakdowns with ORDER BY clauses.</returns>
|
||||
public IEnumerable<QueryBreakdown> WhereHaveOrderByClause()
|
||||
{
|
||||
return _queryBreakdowns.Where(q =>
|
||||
!string.IsNullOrWhiteSpace(q.OrderByClause?.Clause));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a comprehensive analysis of all queries in the collection.
|
||||
/// </summary>
|
||||
/// <returns>Analysis summary for each query.</returns>
|
||||
public IEnumerable<SnowflakeQueryAnalysis> AnalyzeQueries()
|
||||
{
|
||||
return _queryBreakdowns.Select((q, index) => new SnowflakeQueryAnalysis
|
||||
{
|
||||
Index = index,
|
||||
HasSelectClause = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause),
|
||||
HasFromClause = !string.IsNullOrWhiteSpace(q.FromClause?.Clause),
|
||||
HasWhereClause = !string.IsNullOrWhiteSpace(q.WhereClause?.Clause),
|
||||
HasGroupByClause = !string.IsNullOrWhiteSpace(q.GroupByClause?.Clause),
|
||||
HasOrderByClause = !string.IsNullOrWhiteSpace(q.OrderByClause?.Clause),
|
||||
UsesSemiStructuredData = UseSemiStructuredData(q),
|
||||
UsesStageReference = UsesStageReference(q),
|
||||
UsesTimeTravelFeature = UsesTimeTravelFeature(q),
|
||||
UsesSnowflakeFunctions = UsesSnowflakeFunctions(q),
|
||||
UsesExternalData = UsesExternalData(q),
|
||||
ColumnCount = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause) ? q.SelectClause.Clause.Split(',').Length : 0,
|
||||
ParameterCount = q.ParameterList.Count()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the combined Snowflake SQL from all breakdowns.
|
||||
/// </summary>
|
||||
/// <param name="includeSetupFinish">Whether to include setup and finish clauses.</param>
|
||||
/// <returns>The combined Snowflake SQL.</returns>
|
||||
public string GetCombinedSql(bool includeSetupFinish = true)
|
||||
{
|
||||
return GetSnowflakeBatch(includeSetupFinish);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to check if a query uses semi-structured data.
|
||||
/// </summary>
|
||||
private static bool UseSemiStructuredData(QueryBreakdown query)
|
||||
{
|
||||
var sql = query.GetSql().ToUpperInvariant();
|
||||
return sql.Contains("JSON_") ||
|
||||
sql.Contains("OBJECT_") ||
|
||||
sql.Contains("ARRAY_") ||
|
||||
sql.Contains("FLATTEN(");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to check if a query uses stage references.
|
||||
/// </summary>
|
||||
private static bool UsesStageReference(QueryBreakdown query)
|
||||
{
|
||||
return query.GetSql().Contains("@") &&
|
||||
(query.GetSql().Contains("FROM @") || query.GetSql().Contains(" @"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to check if a query uses time travel features.
|
||||
/// </summary>
|
||||
private static bool UsesTimeTravelFeature(QueryBreakdown query)
|
||||
{
|
||||
var sql = query.GetSql().ToUpperInvariant();
|
||||
return sql.Contains("BEFORE (") ||
|
||||
sql.Contains("AT (") ||
|
||||
sql.Contains("MATCH_CONDITION");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to check if a query uses Snowflake-specific functions.
|
||||
/// </summary>
|
||||
private static bool UsesSnowflakeFunctions(QueryBreakdown query)
|
||||
{
|
||||
var sql = query.GetSql().ToUpperInvariant();
|
||||
var snowflakeFunctions = new[]
|
||||
{
|
||||
"PARSE_JSON", "OBJECT_INSERT", "ARRAY_CONSTRUCT", "FLATTEN",
|
||||
"LISTAGG", "APPROX_COUNT_DISTINCT", "HASH", "ZEROIFNULL"
|
||||
};
|
||||
return snowflakeFunctions.Any(func => sql.Contains(func));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to check if a query uses external data.
|
||||
/// </summary>
|
||||
private static bool UsesExternalData(QueryBreakdown query)
|
||||
{
|
||||
var sql = query.GetSql().ToUpperInvariant();
|
||||
return sql.Contains("EXTERNAL TABLE") ||
|
||||
sql.Contains(" FROM @") ||
|
||||
sql.Contains("COPY INTO @");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronizes parameter values across all queries in the collection.
|
||||
/// Ensures that if a parameter with the same name exists in multiple queries, they all have the same value.
|
||||
/// </summary>
|
||||
public void SynchronizeParameters()
|
||||
{
|
||||
// Get all unique parameter names across all queries
|
||||
var allParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var query in _queryBreakdowns)
|
||||
{
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParameterNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
// For each parameter, use the last query's value and sync to all queries that have it
|
||||
foreach (var paramName in allParameterNames)
|
||||
{
|
||||
object? lastValue = null;
|
||||
bool parameterFound = false;
|
||||
|
||||
// Find the last query that has this parameter and get its value
|
||||
for (int i = _queryBreakdowns.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_queryBreakdowns[i].Parameters.ContainsKey(paramName))
|
||||
{
|
||||
lastValue = _queryBreakdowns[i].Parameters[paramName];
|
||||
parameterFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronize the parameter value to all queries that have it
|
||||
if (parameterFound)
|
||||
{
|
||||
foreach (var query in _queryBreakdowns.Where(q => q.Parameters.ContainsKey(paramName)))
|
||||
{
|
||||
query.Parameters[paramName] = lastValue!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a parameter with a specific value to all queries in the collection.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter (without the : or @ prefix).</param>
|
||||
/// <param name="value">The value to assign to the parameter. Can be null.</param>
|
||||
public void AddParameterToAll(string parameterName, object? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parameterName))
|
||||
{
|
||||
throw new ArgumentException("Parameter name cannot be null or empty.", nameof(parameterName));
|
||||
}
|
||||
|
||||
foreach (var query in _queryBreakdowns)
|
||||
{
|
||||
query.Parameters[parameterName] = value!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all unique parameters from all queries in the collection as a combined dictionary.
|
||||
/// </summary>
|
||||
/// <returns>A dictionary containing all unique parameters across all queries.</returns>
|
||||
protected Dictionary<string, object?> GetCombinedParameterDictionary()
|
||||
{
|
||||
var combinedParams = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var query in _queryBreakdowns)
|
||||
{
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
combinedParams[param.Name] = param.Value;
|
||||
}
|
||||
|
||||
// Add/override from Parameters dictionary (manually added parameters)
|
||||
foreach (var param in query.Parameters)
|
||||
{
|
||||
combinedParams[param.Key] = param.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return combinedParams;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a formatted string representation of all unique parameters with Snowflake-specific syntax.
|
||||
/// </summary>
|
||||
/// <param name="includeDataTypes">If true, includes Snowflake data types in the output format.</param>
|
||||
/// <returns>
|
||||
/// A formatted string such as ":paramName = value" or ":paramName = value -- VARIANT"
|
||||
/// for each unique parameter.
|
||||
/// </returns>
|
||||
public string GetParametersAsString(bool includeDataTypes = false)
|
||||
{
|
||||
var parameters = GetCombinedParameterDictionary();
|
||||
if (parameters.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var isFirst = true;
|
||||
|
||||
foreach (var param in parameters.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
sb.AppendLine(",");
|
||||
}
|
||||
|
||||
sb.Append($":{param.Key} = {FormatParameterValue(param.Value)}");
|
||||
|
||||
if (includeDataTypes)
|
||||
{
|
||||
var dataType = GetSnowflakeDataType(param.Value);
|
||||
sb.Append($" -- {dataType}");
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a detailed usage report for all parameters across the queries in the collection.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of ParameterUsageReport objects with usage statistics.</returns>
|
||||
/// <summary>
|
||||
/// Gets a report of parameter usage across all queries.
|
||||
/// </summary>
|
||||
/// <returns>Parameter usage information.</returns>
|
||||
public IEnumerable<ParameterUsageReport> GetParameterUsageReport()
|
||||
{
|
||||
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
||||
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var query in _queryBreakdowns)
|
||||
{
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParamNames.Add(param.Name);
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParamNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var paramName in allParamNames)
|
||||
{
|
||||
var queriesUsing = 0;
|
||||
object? lastValue = null;
|
||||
|
||||
foreach (var query in _queryBreakdowns)
|
||||
{
|
||||
// Check ParameterList first (parsed)
|
||||
var param = query.ParameterList.FirstOrDefault(p => p.Name.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
||||
if (param != null)
|
||||
{
|
||||
queriesUsing++;
|
||||
lastValue = param.Value;
|
||||
}
|
||||
// Also check Parameters dictionary (manually added)
|
||||
else if (query.Parameters.TryGetValue(paramName, out var dictValue))
|
||||
{
|
||||
queriesUsing++;
|
||||
lastValue = dictValue;
|
||||
}
|
||||
}
|
||||
|
||||
yield return new ParameterUsageReport
|
||||
{
|
||||
ParameterName = paramName,
|
||||
Value = lastValue,
|
||||
UsedInQueryCount = queriesUsing,
|
||||
TotalQueries = _queryBreakdowns.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of columns selected across all queries.
|
||||
/// </summary>
|
||||
/// <returns>Total column count.</returns>
|
||||
public int GetTotalSelectedColumns()
|
||||
{
|
||||
return _queryBreakdowns.Sum(q =>
|
||||
!string.IsNullOrWhiteSpace(q.SelectClause?.Clause)
|
||||
? q.SelectClause.Clause.Split(',').Length
|
||||
: 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all unique table names referenced across all queries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This provides a quick overview of which tables are being queried.
|
||||
/// Note: This is a best-effort extraction and may not capture all table references,
|
||||
/// especially in complex subqueries or with aliasing.
|
||||
/// </remarks>
|
||||
/// <returns>List of unique table names.</returns>
|
||||
public IEnumerable<string> GetUniqueTableReferences()
|
||||
{
|
||||
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var tableNames = _queryBreakdowns
|
||||
.Where(q => !string.IsNullOrWhiteSpace(q.FromClause?.Clause))
|
||||
.SelectMany(q => ExtractTableNames(q.FromClause!.Clause!));
|
||||
|
||||
foreach (var table in tableNames)
|
||||
{
|
||||
tables.Add(table);
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary of all queries including their types and basic composition.
|
||||
/// </summary>
|
||||
/// <returns>Summary information for each query.</returns>
|
||||
public IEnumerable<SnowflakeQueryAnalysis> GetQuerySummaries()
|
||||
{
|
||||
var stageQueries = WhereUseStageReference().ToHashSet();
|
||||
var semiStructuredQueries = WhereUseSemiStructuredData().ToHashSet();
|
||||
|
||||
return _queryBreakdowns.Select((q, index) => new SnowflakeQueryAnalysis
|
||||
{
|
||||
Index = index,
|
||||
HasSelectClause = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause),
|
||||
HasFromClause = !string.IsNullOrWhiteSpace(q.FromClause?.Clause),
|
||||
HasWhereClause = !string.IsNullOrWhiteSpace(q.WhereClause?.Clause),
|
||||
HasGroupByClause = !string.IsNullOrWhiteSpace(q.GroupByClause?.Clause),
|
||||
HasOrderByClause = !string.IsNullOrWhiteSpace(q.OrderByClause?.Clause),
|
||||
HasCTE = q.WithClauses.Count > 0,
|
||||
UsesSemiStructuredData = semiStructuredQueries.Contains(q),
|
||||
UsesStageReference = stageQueries.Contains(q),
|
||||
UsesTimeTravelFeature = UsesTimeTravelFeature(q),
|
||||
UsesSnowflakeFunctions = false,
|
||||
UsesExternalData = false,
|
||||
ColumnCount = !string.IsNullOrWhiteSpace(q.SelectClause?.Clause) ? q.SelectClause.Clause.Split(',').Length : 0,
|
||||
ParameterCount = q.ParameterList.Count()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to extract table names from a FROM clause.
|
||||
/// </summary>
|
||||
private static IEnumerable<string> ExtractTableNames(string fromClause)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fromClause))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var parts = fromClause.Split(',');
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var trimmed = part.Trim();
|
||||
var tokens = trimmed.Split(new[] { " AS ", " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (tokens.Length > 0)
|
||||
{
|
||||
var tableName = tokens[0].Trim();
|
||||
if (!string.IsNullOrWhiteSpace(tableName))
|
||||
{
|
||||
yield return tableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert a .NET object to its corresponding Snowflake data type string.
|
||||
/// </summary>
|
||||
private static string GetSnowflakeDataType(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "VARIANT",
|
||||
bool => "BOOLEAN",
|
||||
byte or sbyte or short or ushort or int or uint or long or ulong => "NUMBER",
|
||||
float or double or decimal => "NUMBER",
|
||||
DateTime or DateTimeOffset => "TIMESTAMP_NTZ",
|
||||
TimeSpan => "TIME",
|
||||
string => value.ToString()!.Length > 255 ? "VARCHAR(MAX)" : "VARCHAR(255)",
|
||||
byte[] => "BINARY",
|
||||
_ => "VARIANT"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to format a parameter value for safe inclusion in Snowflake SQL statements.
|
||||
/// </summary>
|
||||
private static string FormatParameterValue(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "NULL",
|
||||
bool b => b ? "TRUE" : "FALSE",
|
||||
string s => $"'{s.Replace("'", "''")}'",
|
||||
DateTime dt => $"'{dt:yyyy-MM-dd HH:mm:ss}'",
|
||||
DateTimeOffset dto => $"'{dto:yyyy-MM-dd HH:mm:ss}'",
|
||||
_ => value.ToString() ?? "NULL"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analysis information about a Snowflake query.
|
||||
/// </summary>
|
||||
public class SnowflakeQueryAnalysis
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the query in the collection.
|
||||
/// </summary>
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query has a SELECT clause.
|
||||
/// </summary>
|
||||
public bool HasSelectClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query has a FROM clause.
|
||||
/// </summary>
|
||||
public bool HasFromClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query has a WHERE clause.
|
||||
/// </summary>
|
||||
public bool HasWhereClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query has a GROUP BY clause.
|
||||
/// </summary>
|
||||
public bool HasGroupByClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query has an ORDER BY clause.
|
||||
/// </summary>
|
||||
public bool HasOrderByClause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query has Common Table Expressions (CTEs).
|
||||
/// </summary>
|
||||
public bool HasCTE { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query uses semi-structured data functions.
|
||||
/// </summary>
|
||||
public bool UsesSemiStructuredData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query references Snowflake stages.
|
||||
/// </summary>
|
||||
public bool UsesStageReference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query uses Snowflake time travel features.
|
||||
/// </summary>
|
||||
public bool UsesTimeTravelFeature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query uses Snowflake-specific functions.
|
||||
/// </summary>
|
||||
public bool UsesSnowflakeFunctions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the query uses external data sources.
|
||||
/// </summary>
|
||||
public bool UsesExternalData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of columns in the SELECT clause.
|
||||
/// </summary>
|
||||
public int ColumnCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of parameters used.
|
||||
/// </summary>
|
||||
public int ParameterCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the query analysis.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Snowflake Query #{Index}");
|
||||
sb.AppendLine($" Basic Structure:");
|
||||
sb.AppendLine($" SELECT: {(HasSelectClause ? "Yes" : "No")} ({ColumnCount} columns)");
|
||||
sb.AppendLine($" FROM: {(HasFromClause ? "Yes" : "No")}");
|
||||
sb.AppendLine($" WHERE: {(HasWhereClause ? "Yes" : "No")}");
|
||||
sb.AppendLine($" GROUP BY: {(HasGroupByClause ? "Yes" : "No")}");
|
||||
sb.AppendLine($" ORDER BY: {(HasOrderByClause ? "Yes" : "No")}");
|
||||
sb.AppendLine($" Snowflake Features:");
|
||||
sb.AppendLine($" Semi-Structured Data: {(UsesSemiStructuredData ? "Yes" : "No")}");
|
||||
sb.AppendLine($" Stage Reference: {(UsesStageReference ? "Yes" : "No")}");
|
||||
sb.AppendLine($" Time Travel: {(UsesTimeTravelFeature ? "Yes" : "No")}");
|
||||
sb.AppendLine($" Snowflake Functions: {(UsesSnowflakeFunctions ? "Yes" : "No")}");
|
||||
sb.AppendLine($" External Data: {(UsesExternalData ? "Yes" : "No")}");
|
||||
sb.Append($" Parameters: {ParameterCount}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports parameter usage statistics across queries in a Snowflake collection.
|
||||
/// </summary>
|
||||
public class ParameterUsageReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the parameter.
|
||||
/// </summary>
|
||||
public string ParameterName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current value of the parameter.
|
||||
/// </summary>
|
||||
public object? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of queries using this parameter.
|
||||
/// </summary>
|
||||
public int UsedInQueryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of queries in the collection.
|
||||
/// </summary>
|
||||
public int TotalQueries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this parameter is used in all queries.
|
||||
/// </summary>
|
||||
public bool IsUsedInAllQueries => UsedInQueryCount == TotalQueries && TotalQueries > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the parameter usage report.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
if (TotalQueries == 0)
|
||||
{
|
||||
return $"{ParameterName}: No queries";
|
||||
}
|
||||
|
||||
var percentage = (UsedInQueryCount * 100.0) / TotalQueries;
|
||||
var valueStr = Value switch
|
||||
{
|
||||
null => "NULL",
|
||||
string s => $"'{s}'",
|
||||
_ => Value.ToString() ?? "NULL"
|
||||
};
|
||||
|
||||
return $"{ParameterName} = {valueStr} ({UsedInQueryCount}/{TotalQueries} queries - {percentage:F1}%)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// A trace listener that writes trace messages to a Snowflake database.
|
||||
/// </summary>
|
||||
public class TraceListener : System.Diagnostics.TraceListener
|
||||
{
|
||||
private readonly string _serverName;
|
||||
private readonly string _traceDbConnectionString;
|
||||
private readonly string _providerName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TraceListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverName">The server name for logging.</param>
|
||||
/// <param name="traceDbConnectionString">The connection string to the trace database.</param>
|
||||
/// <param name="providerName">The provider name (default: "Snowflake.Data.Client").</param>
|
||||
public TraceListener(string serverName, string traceDbConnectionString, string providerName = "Snowflake.Data.Client")
|
||||
{
|
||||
_serverName = serverName;
|
||||
_traceDbConnectionString = traceDbConnectionString;
|
||||
_providerName = providerName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a message to the trace database.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
public override void Write(string? message)
|
||||
{
|
||||
WriteTrace(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a message followed by a line terminator to the trace database.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
public override void WriteLine(string? message)
|
||||
{
|
||||
WriteTrace(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a trace message to the Snowflake database.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write.</param>
|
||||
private void WriteTrace(string? message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var factory = DbProviderFactories.GetFactory(_providerName);
|
||||
using var connection = factory.CreateConnection();
|
||||
|
||||
if (connection == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
connection.ConnectionString = _traceDbConnectionString;
|
||||
connection.Open();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandType = CommandType.Text;
|
||||
command.CommandText = "INSERT INTO Trace (SERVER, MESSAGE) VALUES(:SERVER, :MESSAGE)";
|
||||
|
||||
var serverParam = command.CreateParameter();
|
||||
serverParam.ParameterName = "SERVER";
|
||||
serverParam.Value = _serverName;
|
||||
command.Parameters.Add(serverParam);
|
||||
|
||||
var messageParam = command.CreateParameter();
|
||||
messageParam.ParameterName = "MESSAGE";
|
||||
messageParam.Value = message ?? (object)DBNull.Value;
|
||||
command.Parameters.Add(messageParam);
|
||||
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using SqlServerUpdateBreakdown = Strata.SqlTools.Breakdowns.SqlServer.UpdateBreakdown;
|
||||
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
|
||||
|
||||
namespace Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an UPDATE SQL statement breakdown with SET, FROM, and WHERE clauses for Snowflake.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class UpdateBreakdown : SqlServerUpdateBreakdown
|
||||
{
|
||||
private static readonly StatementParser SnowflakeParserInstance = new StatementParser();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UpdateBreakdown"/> class.
|
||||
/// </summary>
|
||||
public UpdateBreakdown() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UpdateBreakdown"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <param name="setClause">The SET clause.</param>
|
||||
/// <param name="whereClause">The WHERE clause.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
public UpdateBreakdown(string tableName, string setClause, string whereClause, bool isMicrosoftSql = false)
|
||||
: base()
|
||||
{
|
||||
var parser = isMicrosoftSql ? Parser : SnowflakeParserInstance;
|
||||
|
||||
var cleanTable = parser.ExtractSqlComments(tableName, out var tableComments);
|
||||
TableName.Clause = cleanTable.Trim();
|
||||
TableName.Comment = tableComments.Count > 0 ? string.Join(" ", tableComments) : null;
|
||||
|
||||
var cleanSet = parser.ExtractSqlComments(setClause, out var setComments);
|
||||
SetClause.Clause = cleanSet.Trim();
|
||||
SetClause.Comment = setComments.Count > 0 ? string.Join(" ", setComments) : null;
|
||||
|
||||
var cleanWhere = parser.ExtractSqlComments(whereClause, out var whereComments);
|
||||
WhereClause.Clause = cleanWhere.Trim();
|
||||
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string for Snowflake.
|
||||
/// </summary>
|
||||
/// <returns>The UPDATE SQL statement.</returns>
|
||||
protected override string GetSqlBreakdown()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("UPDATE ");
|
||||
sb.AppendLine($" {TableName.Clause}");
|
||||
|
||||
sb.AppendLine("SET ");
|
||||
sb.AppendLine($" {SetClause.Clause}");
|
||||
|
||||
if (IsUsingFromClause)
|
||||
{
|
||||
// Snowflake supports FROM clause in UPDATE
|
||||
sb.AppendLine("FROM ");
|
||||
sb.AppendLine($" {FromClause.Clause}");
|
||||
}
|
||||
|
||||
if (IsUsingWhereClause)
|
||||
{
|
||||
sb.AppendLine("WHERE ");
|
||||
sb.AppendLine($" {WhereClause.Clause}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Snowflake UPDATE SQL statement into an UpdateBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The UPDATE SQL statement to parse.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules. If false, uses Snowflake parsing rules. Defaults to false.</param>
|
||||
/// <returns>An UpdateBreakdown object representing the parsed statement.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when sql is null or empty.</exception>
|
||||
/// <exception cref="FormatException">Thrown when the SQL statement cannot be parsed.</exception>
|
||||
public static UpdateBreakdown Parse(string sql, bool isMicrosoftSql = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sql), "SQL statement cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (!TryParse(sql, out var result, out var error, isMicrosoftSql))
|
||||
{
|
||||
throw new FormatException($"Failed to parse {(isMicrosoftSql ? "T-SQL" : "Snowflake SQL")} UPDATE statement: {error}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake UPDATE SQL statement into an UpdateBreakdown object.
|
||||
/// </summary>
|
||||
/// <param name="sql">The UPDATE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed UpdateBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out UpdateBreakdown result, bool isMicrosoftSql = false)
|
||||
=> TryParse(sql, out result, out _, isMicrosoftSql);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Snowflake UPDATE SQL statement into an UpdateBreakdown object.
|
||||
/// Handles Snowflake-specific syntax.
|
||||
/// </summary>
|
||||
/// <param name="sql">The UPDATE SQL statement to parse.</param>
|
||||
/// <param name="result">When this method returns, contains the parsed UpdateBreakdown if successful, or null if parsing failed.</param>
|
||||
/// <param name="errorMessage">When this method returns false, contains a message describing why parsing failed.</param>
|
||||
/// <param name="isMicrosoftSql">If true, uses Microsoft T-SQL parsing rules instead of Snowflake rules. Defaults to false.</param>
|
||||
/// <returns>true if the SQL was successfully parsed; otherwise, false.</returns>
|
||||
public static bool TryParse(string sql, out UpdateBreakdown result, out string errorMessage, bool isMicrosoftSql = false)
|
||||
{
|
||||
result = null!;
|
||||
errorMessage = null!;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
errorMessage = "SQL statement cannot be null or empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// If Microsoft SQL mode, delegate to base class
|
||||
if (isMicrosoftSql)
|
||||
{
|
||||
if (!SqlServerUpdateBreakdown.TryParse(sql, out var baseResult, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert to Snowflake UpdateBreakdown
|
||||
result = new UpdateBreakdown
|
||||
{
|
||||
TableName = baseResult.TableName,
|
||||
SetClause = baseResult.SetClause,
|
||||
FromClause = baseResult.FromClause,
|
||||
WhereClause = baseResult.WhereClause,
|
||||
SetupClauses = baseResult.SetupClauses,
|
||||
FinishClauses = baseResult.FinishClauses
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var parser = SnowflakeParserInstance;
|
||||
sql = parser.NormalizeSqlPreservingComments(sql);
|
||||
|
||||
// Check if it's an UPDATE statement
|
||||
var sqlTrimmed = sql.TrimStart();
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
errorMessage = "SQL statement must start with UPDATE.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract setup and finish clauses
|
||||
var setupClauses = new List<string>();
|
||||
sql = parser.ExtractSetupClauses(sql, setupClauses);
|
||||
|
||||
var finishClauses = new ArrayList();
|
||||
sql = parser.ExtractFinishClauses(sql, finishClauses);
|
||||
|
||||
// Parse UPDATE statement - handle both with and without FROM clause
|
||||
var updateMatch = System.Text.RegularExpressions.Regex.Match(sql,
|
||||
@"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
if (!updateMatch.Success)
|
||||
{
|
||||
errorMessage = "Could not parse UPDATE statement. Expected format: UPDATE table SET column=value [FROM table] [WHERE condition]";
|
||||
return false;
|
||||
}
|
||||
|
||||
var tableName = updateMatch.Groups[1].Value.Trim();
|
||||
var setClause = updateMatch.Groups[2].Value.Trim();
|
||||
var fromClause = updateMatch.Groups.Count > 3 ? updateMatch.Groups[3].Value.Trim() : string.Empty;
|
||||
var whereClause = updateMatch.Groups.Count > 4 ? updateMatch.Groups[4].Value.Trim() : string.Empty;
|
||||
|
||||
result = new UpdateBreakdown(tableName, setClause, whereClause, isMicrosoftSql: false)
|
||||
{
|
||||
SetupClauses = setupClauses,
|
||||
FinishClauses = finishClauses
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fromClause))
|
||||
{
|
||||
var cleanFrom = parser.ExtractSqlComments(fromClause, out var fromComments);
|
||||
result.FromClause.Clause = cleanFrom.Trim();
|
||||
result.FromClause.Comment = fromComments.Count > 0 ? string.Join(" ", fromComments) : null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error during parsing: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Snowflake-specific factory class for creating boolean expressions and SQL filter conditions from Filter objects.
|
||||
/// Inherits from the SQL Server implementation and extends it with Snowflake-specific syntax support.
|
||||
/// </summary>
|
||||
public abstract class ExpressionFactory : SqlServer.ExpressionFactory.ExpressionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExpressionFactory"/> class with the default system time provider.
|
||||
/// </summary>
|
||||
protected ExpressionFactory() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExpressionFactory"/> class with the specified time provider.
|
||||
/// </summary>
|
||||
/// <param name="timeProvider">The time provider implementation for date/time operations.</param>
|
||||
protected ExpressionFactory(TimeProvider timeProvider) : base(timeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
// Snowflake-specific expression methods can be added here as needed
|
||||
// For example, support for Snowflake-specific date functions, parameter syntax (@param and :param), etc.
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum AggregationType
|
||||
{
|
||||
Sum = 0,
|
||||
Count = 1,
|
||||
CountDistinct = 2,
|
||||
Avg = 3,
|
||||
Median = 4,
|
||||
Min = 5,
|
||||
Max = 6
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class CalculationFilter : Filter
|
||||
{
|
||||
public IEnumerable<string> AliasedDataColumnIds { get; }
|
||||
|
||||
public CalculationFilter(int dataColumnId, IEnumerable<string> aliasedDataColumnIds, IEnumerable<object> values, IEnumerable<FilterCondition> conditions)
|
||||
: base(dataColumnId, FilterType.Conditions, values, conditions, DatePart.Continuous, false, 0, 0)
|
||||
{
|
||||
AliasedDataColumnIds = aliasedDataColumnIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class CalculationFilterGroup
|
||||
{
|
||||
[JsonIgnore]
|
||||
private IEnumerable<CalculationFilter> _filters;
|
||||
|
||||
// Hereditary logical operation applied to all Filters
|
||||
public LogicalOperator LogicalOperator { get; set; }
|
||||
|
||||
public IEnumerable<CalculationFilter> Filters
|
||||
{
|
||||
get => _filters?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilter>();
|
||||
set => _filters = value;
|
||||
}
|
||||
|
||||
public CalculationFilterGroup()
|
||||
{
|
||||
LogicalOperator = LogicalOperator.And;
|
||||
_filters = new List<CalculationFilter>();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public CalculationFilterGroup(IEnumerable<CalculationFilter> filters, LogicalOperator logicalOperator)
|
||||
{
|
||||
_filters = filters;
|
||||
LogicalOperator = logicalOperator;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Filters != null && Filters.Any();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class ColumnQueryConfig
|
||||
{
|
||||
public int DataColumnId { get; set; }
|
||||
|
||||
public DatePart DatePart { get; set; }
|
||||
|
||||
public Filter? Filter { get; set; }
|
||||
|
||||
public int RowLimit { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
public ColumnQueryConfig(int dataColumnId, DatePart datePart, Filter? filter, int rowLimit)
|
||||
{
|
||||
DataColumnId = dataColumnId;
|
||||
DatePart = datePart;
|
||||
Filter = filter != null && filter.IsValid() ? filter : null;
|
||||
RowLimit = rowLimit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum DatePart
|
||||
{
|
||||
Continuous = 0,
|
||||
Year,
|
||||
Quarter,
|
||||
Month,
|
||||
Week,
|
||||
Day,
|
||||
FiscalYear,
|
||||
FiscalQuarter
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class Field
|
||||
{
|
||||
public string ColumnAlias { get; set; } = string.Empty;
|
||||
public int DataColumnId { get; set; }
|
||||
public DatePart DatePart { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a filter criteria for querying data with support for various filter types including lists, date ranges, and timeframes.
|
||||
/// Filters can be applied to specific data columns and support different date granularities.
|
||||
/// </summary>
|
||||
public class Filter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the identifier of the data column to which this filter applies.
|
||||
/// </summary>
|
||||
public int DataColumnId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of filter being applied (e.g., List, Calendar, Timeframe).
|
||||
/// </summary>
|
||||
public FilterType FilterType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of values to filter by. The interpretation depends on the <see cref="FilterType"/>.
|
||||
/// </summary>
|
||||
public IEnumerable<object> Values { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of filter conditions that define complex filtering logic.
|
||||
/// Only valid conditions are retained.
|
||||
/// </summary>
|
||||
public IEnumerable<FilterCondition> Conditions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date granularity part for date-based filtering (e.g., Year, Month, Day, FiscalYear).
|
||||
/// </summary>
|
||||
public DatePart DatePart { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use NOT IN instead of IN for list-type filters.
|
||||
/// Only applies when <see cref="FilterType"/> is List.
|
||||
/// </summary>
|
||||
public bool ListUseNotIn { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offset from the current time for timeframe-based filters.
|
||||
/// Used in conjunction with <see cref="DateTimeFrameCount"/> to define relative time periods.
|
||||
/// </summary>
|
||||
public int DateTimeFrameOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the zero-based number of time increments from the offset.
|
||||
/// A value of 0 means current period, -1 means one period backward, and 1 means one period forward.
|
||||
/// The unit (year, month, day, etc.) is determined by the <see cref="DatePart"/> property.
|
||||
/// </summary>
|
||||
public int DateTimeFrameCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Filter"/> class with the specified filter criteria.
|
||||
/// </summary>
|
||||
/// <param name="dataColumnId">The identifier of the data column to filter.</param>
|
||||
/// <param name="filterType">The type of filter to apply.</param>
|
||||
/// <param name="values">The collection of values for the filter.</param>
|
||||
/// <param name="conditions">The collection of filter conditions (invalid conditions are automatically removed).</param>
|
||||
/// <param name="datePart">The date granularity for date-based filtering.</param>
|
||||
/// <param name="listUseNotIn">Whether to use NOT IN for list filters; false to use IN.</param>
|
||||
/// <param name="dateTimeFrameOffset">The offset from current time for timeframe filters.</param>
|
||||
/// <param name="dateTimeFrameCount">The number of time increments from the offset (0 = current, negative = past, positive = future).</param>
|
||||
[JsonConstructor]
|
||||
public Filter(int dataColumnId, FilterType filterType, IEnumerable<object> values, IEnumerable<FilterCondition> conditions, DatePart datePart, bool listUseNotIn, int dateTimeFrameOffset, int dateTimeFrameCount)
|
||||
{
|
||||
DataColumnId = dataColumnId;
|
||||
FilterType = filterType;
|
||||
Values = values;
|
||||
Conditions = conditions.Where(x => x.IsValid()).ToList();
|
||||
DatePart = datePart;
|
||||
ListUseNotIn = listUseNotIn;
|
||||
DateTimeFrameOffset = dateTimeFrameOffset;
|
||||
DateTimeFrameCount = dateTimeFrameCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this filter has valid criteria that can be applied.
|
||||
/// A filter is valid if it has values, conditions, or non-default timeframe settings.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the filter has values, conditions, or timeframe settings; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool IsValid()
|
||||
{
|
||||
return (Values != null && Values.Any()) || (Conditions != null && Conditions.Any()) || (DateTimeFrameCount != default || DateTimeFrameOffset != default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class FilterCondition
|
||||
{
|
||||
public FilterOperator Operator { get; set; }
|
||||
|
||||
public IEnumerable<object>? Values { get; set; }
|
||||
|
||||
// This is not hereditary to Values; it is used for combination with the next FilterCondition in the set
|
||||
// todo: That could be indexed to ensure accuracy
|
||||
public LogicalOperator LogicalOperator { get; set; }
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Values != null && Values.Any();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class FilterGroup
|
||||
{
|
||||
// Hereditary logical operation applied to all Filters
|
||||
public LogicalOperator LogicalOperator { get; set; }
|
||||
|
||||
public IEnumerable<Filter> Filters { get; }
|
||||
|
||||
public FilterGroup()
|
||||
{
|
||||
LogicalOperator = LogicalOperator.And;
|
||||
Filters = new List<Filter>();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public FilterGroup(IEnumerable<Filter> filters, LogicalOperator logicalOperator)
|
||||
{
|
||||
Filters = filters.Where(x => x.IsValid()).ToList();
|
||||
LogicalOperator = logicalOperator;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Filters != null && Filters.Any();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum FilterOperator
|
||||
{
|
||||
Equals = 0,
|
||||
NotEquals = 1,
|
||||
LessThan = 2,
|
||||
LessThanOrEqualTo = 3,
|
||||
GreaterThan = 4,
|
||||
GreaterThanOrEqualTo = 5,
|
||||
Between = 6, // this is a function, not a comparison - x BETWEEN a AND b is the same as: x >= a AND x <= z
|
||||
Contains = 7,
|
||||
StartsWith = 8,
|
||||
EndsWith = 9
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum FilterType
|
||||
{
|
||||
List = 0,
|
||||
Conditions = 1,
|
||||
Calendar = 2,
|
||||
Timeframe = 3
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public enum LogicalOperator
|
||||
{
|
||||
[Display(Name = "and")]
|
||||
And,
|
||||
[Display(Name = "or")]
|
||||
Or
|
||||
}
|
||||
|
||||
public static class LogicalOperatorExtensions
|
||||
{
|
||||
public static string ToSql(this LogicalOperator logicalOperator, bool withSpaces = true)
|
||||
{
|
||||
var sql = "";
|
||||
switch (logicalOperator)
|
||||
{
|
||||
case LogicalOperator.And:
|
||||
sql = "and";
|
||||
break;
|
||||
case LogicalOperator.Or:
|
||||
sql = "or";
|
||||
break;
|
||||
}
|
||||
|
||||
return withSpaces ? $" {sql} " : sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
|
||||
|
||||
public class QueryConfig
|
||||
{
|
||||
public IEnumerable<Row> Rows { get; set; }
|
||||
|
||||
public IEnumerable<Value> Values { get; set; }
|
||||
|
||||
public IEnumerable<FilterGroup> FilterGroups { get; }
|
||||
|
||||
public bool WithTotals { get; set; }
|
||||
|
||||
public int RowLimit { get; set; }
|
||||
|
||||
public QueryConfig()
|
||||
{
|
||||
Rows = new List<Row>();
|
||||
Values = new List<Value>();
|
||||
FilterGroups = new List<FilterGroup>();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public QueryConfig(IEnumerable<FilterGroup> filterGroups, IEnumerable<Row> rows, IEnumerable<Value> values, bool withTotals, int rowLimit)
|
||||
{
|
||||
FilterGroups = filterGroups.Where(x => x.IsValid()).ToList();
|
||||
Rows = rows;
|
||||
Values = values;
|
||||
WithTotals = withTotals;
|
||||
RowLimit = rowLimit;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user