chore: initial git load of code space
This commit is contained in:
@@ -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}%)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user