refactor(query): extract shared Strata.SqlTools.Query model project

Moves the byte-identical 19-file ExpressionFactory/Query tree (duplicated
across Snowflake and SqlServer) into the new Strata.SqlTools.Query project
under the flat namespace Strata.SqlTools.Query. Both dialect projects now
reference the shared project; the Snowflake copies are deleted.

Also folds in the IDE0028 fix on CalculationFilterGroup.GetValidFilters
(collection expression []), which resolves both new-code IDE0028 smells
in one place now that there is a single copy.

Eliminates the 63 new duplicate lines flagged on the PR and removes the
largest contributor to the project's 11.1% duplication density. No
behavioral change: the moved types are identical to the originals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thom Lamb
2026-05-20 12:04:01 -05:00
co-authored by Claude Opus 4.7
parent c5a4a4b02b
commit 5cb32d2311
44 changed files with 36 additions and 445 deletions
@@ -1,12 +0,0 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum AggregationType
{
Sum = 0,
Count = 1,
CountDistinct = 2,
Avg = 3,
Median = 4,
Min = 5,
Max = 6
}
@@ -1,12 +0,0 @@
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;
}
}
@@ -1,34 +0,0 @@
using System.Text.Json.Serialization;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class CalculationFilterGroup
{
// Hereditary logical operation applied to all Filters
public LogicalOperator LogicalOperator { get; set; }
public IEnumerable<CalculationFilter> Filters { get; set; }
public CalculationFilterGroup()
{
LogicalOperator = LogicalOperator.And;
Filters = new List<CalculationFilter>();
}
[JsonConstructor]
public CalculationFilterGroup(IEnumerable<CalculationFilter> filters, LogicalOperator logicalOperator)
{
Filters = filters;
LogicalOperator = logicalOperator;
}
public IEnumerable<CalculationFilter> GetValidFilters()
{
return Filters?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilter>();
}
public bool IsValid()
{
return Filters != null && Filters.Any(x => x.IsValid());
}
}
@@ -1,23 +0,0 @@
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;
}
}
@@ -1,13 +0,0 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum DatePart
{
Continuous = 0,
Year,
Quarter,
Month,
Week,
Day,
FiscalYear,
FiscalQuarter
}
@@ -1,8 +0,0 @@
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; }
}
@@ -1,91 +0,0 @@
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);
}
}
@@ -1,17 +0,0 @@
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();
}
}
@@ -1,29 +0,0 @@
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();
}
}
@@ -1,15 +0,0 @@
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
}
@@ -1,9 +0,0 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum FilterType
{
List = 0,
Conditions = 1,
Calendar = 2,
Timeframe = 3
}
@@ -1,30 +0,0 @@
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;
}
}
@@ -1,33 +0,0 @@
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;
}
}
@@ -1,20 +0,0 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public static class QueryConfigExtensions
{
/// <summary>
/// Gets all column ids referenced by this <see cref="QueryConfig"/>
/// </summary>
/// <param name="queryConfig"></param>
/// <returns></returns>
public static int[] GetAllColumnIds(this QueryConfig queryConfig)
{
return queryConfig.Values.SelectMany(value => value.CalculationDataColumnIds)
.Union(queryConfig.Values.SelectMany(x => x.FilterGroups.SelectMany(y => y.GetValidFilters().Select(f => f.DataColumnId))))
.Union(queryConfig.Rows.Select(row => row.DataColumnId))
// FilterGroup.Filters is pre-filtered at construction (see FilterGroup.cs JsonConstructor); no GetValidFilters() equivalent is needed here.
.Union(queryConfig.FilterGroups.SelectMany(filterGroup => filterGroup.Filters.Select(filter => filter.DataColumnId)))
.ToArray();
}
}
@@ -1,11 +0,0 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class Row : Field
{
public SortDirection sortDirection { get; set; }
public Row()
{
sortDirection = SortDirection.Asc;
}
}
@@ -1,15 +0,0 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public enum SortDirection
{
Asc = 0,
Desc = 1
}
public static class SortDirectionExtensions
{
public static string ToSql(this SortDirection sortDirection)
{
return sortDirection.ToString().ToUpper();
}
}
@@ -1,13 +0,0 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class SqlResponse
{
public string SqlQuery { get; }
public IEnumerable<KeyValuePair<string, object>> Parameters { get; }
public SqlResponse(string sqlQuery, IEnumerable<KeyValuePair<string, object>> parameters)
{
SqlQuery = sqlQuery;
Parameters = parameters;
}
}
@@ -1,28 +0,0 @@
using System.Text.Json.Serialization;
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class Value
{
public string ColumnAlias { get; }
public string Calculation { get; }
public IEnumerable<int> CalculationDataColumnIds { get; }
public IEnumerable<string> AliasedIds { get; }
public IEnumerable<CalculationFilterGroup> FilterGroups { get; }
public Value() : this(string.Empty, string.Empty, new int[0], new string[0], new CalculationFilterGroup[0])
{
// FilterGroups = new List<CalculationFilterGroup>();
}
[JsonConstructor]
public Value(string columnAlias, string calculation, IEnumerable<int> calculationDataColumnIds, IEnumerable<string> aliasedIds, IEnumerable<CalculationFilterGroup> filterGroups)
{
ColumnAlias = columnAlias;
Calculation = calculation;
CalculationDataColumnIds = calculationDataColumnIds ?? Array.Empty<int>();
AliasedIds = aliasedIds ?? aliasedIds ?? Array.Empty<string>();
FilterGroups = filterGroups?.Where(x => x.IsValid()).ToList() ?? new List<CalculationFilterGroup>();
}
}
@@ -1,8 +0,0 @@
namespace Strata.SqlTools.Snowflake.ExpressionFactory.Query;
public class ValueFilter
{
public int DataColumnId { get; set; }
public object? FilterValue { get; set; }
}