From 08d283e9dcbc53533cf1ab7c1eb3d1072ef24874 Mon Sep 17 00:00:00 2001 From: anderslyman Date: Mon, 22 Apr 2019 20:55:00 -0600 Subject: [PATCH] pivot and unpivot query parsing fixes --- ColumnExtractor/Parser.cs | 16 +- ColumnExtractor/Traverse/Column.cs | 43 +++-- ColumnExtractor/Traverse/Cte.cs | 3 - ColumnExtractor/Traverse/Traverser.cs | 226 ++++++++++++++++++------- SqlParser.linq | 40 ++++- UnitTests/Tests.cs | 231 +++++++------------------- 6 files changed, 296 insertions(+), 263 deletions(-) diff --git a/ColumnExtractor/Parser.cs b/ColumnExtractor/Parser.cs index ee9d3e9..4239059 100644 --- a/ColumnExtractor/Parser.cs +++ b/ColumnExtractor/Parser.cs @@ -12,20 +12,22 @@ public class Parser private readonly bool _log; private readonly bool _outputSelectStar; private readonly bool _bracketOutput; + private readonly bool _outputPivotGeneratedColumns; - public Parser() + public Parser() { _log = false; _outputSelectStar = false; _bracketOutput = false; } - public Parser(bool log, bool outputSelectStar, bool bracketOutput) + public Parser(bool log, bool outputSelectStar, bool bracketOutput, bool outputPivotGeneratedColumns = false) { _log = log; _outputSelectStar = outputSelectStar; _bracketOutput = bracketOutput; - } + _outputPivotGeneratedColumns = outputPivotGeneratedColumns; + } public ParsedData GetColumns(string sql) { @@ -37,7 +39,9 @@ public ParsedData GetColumns(string sql) var result = parser.Parse(reader, out var errors); var traverser = new Traverser(_log); var columnData = traverser.TraverseObject(result, null).Distinct().ToArray(); - var tableData = columnData.Where(c => c?.AmbiguousTableReferences != null) + var tableData = columnData + .Where(c => _outputPivotGeneratedColumns || !c.IsPivotGeneratedColumn) + .Where(c => c?.AmbiguousTableReferences != null) .SelectMany(c => c.AmbiguousTableReferences) .Concat(columnData.Select(c => c.AbsoluteTableReference)) .Where(c => c != null) @@ -87,6 +91,7 @@ public TableData[] GetTables(string sql) foreach (var table in tableData) { table.Columns = columnData + .Where(c => _outputPivotGeneratedColumns || !c.IsPivotGeneratedColumn) .Where(c => c.AbsoluteTableReference != null && table.Equals(new TableData(c.AbsoluteTableReference, _bracketOutput))) .Select(c => new ColumnData(c, _bracketOutput)) .Where(cd => _outputSelectStar || cd.ColumnNM != "*") @@ -94,7 +99,8 @@ public TableData[] GetTables(string sql) .ToArray(); table.PossibleColumns = columnData - .Where(c => c.AmbiguousTableReferences != null && c.AmbiguousTableReferences.Any(t => table.Equals(new TableData(t, _bracketOutput)))) + .Where(c => _outputPivotGeneratedColumns || !c.IsPivotGeneratedColumn) + .Where(c => c.AmbiguousTableReferences != null && c.AmbiguousTableReferences.Any(t => table.Equals(new TableData(t, _bracketOutput)))) .Select(c => new ColumnData(c, _bracketOutput)) .Where(cd => _outputSelectStar || cd.ColumnNM != "*") .Distinct() diff --git a/ColumnExtractor/Traverse/Column.cs b/ColumnExtractor/Traverse/Column.cs index 103aced..52c79ba 100644 --- a/ColumnExtractor/Traverse/Column.cs +++ b/ColumnExtractor/Traverse/Column.cs @@ -11,6 +11,8 @@ public class Column public Table AbsoluteTableReference; public IEnumerable AmbiguousTableReferences; public IEnumerable CteReferences; + public bool IsPivotGeneratedColumn; + public string FullyQualifiedName => GetFullyQualifiedName(true); public string GetFullyQualifiedName(bool brackets) @@ -27,22 +29,29 @@ public string GetFullyQualifiedName(bool brackets) : availableParts.Any() ? $"{string.Join(".", availableParts)}" : null; } - public override bool Equals(object obj) - { - return obj is Column summary && - Name.Equals(summary.Name, StringComparison.OrdinalIgnoreCase) && - EqualityComparer
.Default.Equals(AbsoluteTableReference, summary.AbsoluteTableReference) && - EqualityComparer>.Default.Equals(AmbiguousTableReferences, - summary.AmbiguousTableReferences); - } + protected bool Equals(Column other) + { + return string.Equals(Name, other.Name) && Equals(AbsoluteTableReference, other.AbsoluteTableReference) && Equals(AmbiguousTableReferences, other.AmbiguousTableReferences) && IsPivotGeneratedColumn == other.IsPivotGeneratedColumn; + } - public override int GetHashCode() - { - var hashCode = 1102383335; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Name != null ? Name.ToLowerInvariant() : null); - hashCode = hashCode * -1521134295 + EqualityComparer
.Default.GetHashCode(AbsoluteTableReference); - hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(AmbiguousTableReferences); - return hashCode; - } - } + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Column) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (Name != null ? Name.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (AbsoluteTableReference != null ? AbsoluteTableReference.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (AmbiguousTableReferences != null ? AmbiguousTableReferences.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ IsPivotGeneratedColumn.GetHashCode(); + return hashCode; + } + } + } } \ No newline at end of file diff --git a/ColumnExtractor/Traverse/Cte.cs b/ColumnExtractor/Traverse/Cte.cs index 8841f3b..3384f24 100644 --- a/ColumnExtractor/Traverse/Cte.cs +++ b/ColumnExtractor/Traverse/Cte.cs @@ -4,21 +4,18 @@ namespace ColumnExtractor.Traverse { public class Cte { - public string Alias; public string Name; public List
LinkedTables = new List
(); public override bool Equals(object obj) { return obj is Cte summary && - Alias == summary.Alias && Name == summary.Name; } public override int GetHashCode() { var hashCode = -601341547; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Alias); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Name != null ? Name.ToLowerInvariant() : null); return hashCode; } diff --git a/ColumnExtractor/Traverse/Traverser.cs b/ColumnExtractor/Traverse/Traverser.cs index 4d92b6e..e2053ed 100644 --- a/ColumnExtractor/Traverse/Traverser.cs +++ b/ColumnExtractor/Traverse/Traverser.cs @@ -149,8 +149,12 @@ public IEnumerable TraverseObject(object obj, Cte[] ctes, Table[] parent } } - return columns; - } + // Remove pivot generated column duplicates + return columns + .OrderByDescending(c => c.IsPivotGeneratedColumn) + .GroupBy(c => c.FullyQualifiedName) + .Select(grp => grp.First()); + } public Table GetTableFromReference(NamedTableReference namedTableReference) { @@ -169,7 +173,7 @@ public Table GetTableFromReference(NamedTableReference namedTableReference) return table; } - public Column GetColumnFromIdentifiers(string[] identifiers, string alias, IEnumerable
containers, IEnumerable
parentContainers, IEnumerable ctes, bool inPivot = false) + public Column GetColumnFromIdentifiers(string[] identifiers, string alias, IEnumerable
containers, IEnumerable
parentContainers, IEnumerable ctes, bool inPivot = false) { if (identifiers == null) return null; @@ -177,7 +181,7 @@ public Column GetColumnFromIdentifiers(string[] identifiers, string alias, IEnum if (parentContainers == null) parentContainers = new Table[0]; if (ctes == null) ctes = new Cte[0]; - Dump($"{_pad}PARSING COLUMN: tables: { string.Join(", ", containers.Count()) }, parent tables: { string.Join(", ", parentContainers.Count()) }, ctes: { string.Join(", ", ctes.Count()) }"); + Dump($"{_pad}PARSING COLUMN: tables: { string.Join(", ", containers.Select(c => c.Name)) }, parent tables: { string.Join(", ", parentContainers.Select(c => c.Name)) }, ctes: { string.Join(", ", ctes.Select(c => c.Name)) }"); var allTables = containers.Concat(parentContainers).Distinct().ToArray(); @@ -194,20 +198,24 @@ public Column GetColumnFromIdentifiers(string[] identifiers, string alias, IEnum if (identifiers.Length == 1) { - matches.AddRange(containers); cteMatches.AddRange(ctes); linkedMatches.AddRange(cteMatches.Where(cte => cte.LinkedTables.Count == 1 && (cte.LinkedTables.First().SelectColumns.Contains(column.Name.ToLowerInvariant()) - || cte.LinkedTables.First().SelectColumns.Contains("*") - || inPivot)) - .SelectMany(cte => cte.LinkedTables) - .Distinct()); - matches.AddRange(cteMatches.Where(cte => cte.LinkedTables.Any(lt => - lt.PossibleSelectColumns.Contains(column.Name.ToLowerInvariant()) - || lt.PossibleSelectColumns.Contains("*") + || inPivot)) .SelectMany(cte => cte.LinkedTables) .Distinct()); + + if (!linkedMatches.Any()) + { + matches.AddRange(containers); + matches.AddRange(cteMatches.Where(cte => cte.LinkedTables.Any(lt => + lt.PossibleSelectColumns.Contains(column.Name.ToLowerInvariant()) + || lt.PossibleSelectColumns.Contains("*") + || inPivot)) + .SelectMany(cte => cte.LinkedTables) + .Distinct()); + } } else if (identifiers.Length == 2) { @@ -226,24 +234,26 @@ public Column GetColumnFromIdentifiers(string[] identifiers, string alias, IEnum matches.Add(aliasMatches.First()); } - cteMatches.AddRange(ctes.Where(cte => !string.IsNullOrWhiteSpace(cte.Alias) && cte.Alias.Equals(tableNameOrAlias, StringComparison.OrdinalIgnoreCase) - || cte.Alias == null && cte.Name.Equals(tableNameOrAlias, StringComparison.OrdinalIgnoreCase))); + cteMatches.AddRange(ctes); + //cteMatches.AddRange(ctes.Where(cte => !string.IsNullOrWhiteSpace(cte.Name) && cte.Name.Equals(tableNameOrAlias, StringComparison.OrdinalIgnoreCase))); linkedMatches.AddRange(cteMatches .Where(cte => cte.LinkedTables.Count == 1 && (cte.LinkedTables.First().SelectColumns.Contains(column.Name.ToLowerInvariant()) - || cte.LinkedTables.First().SelectColumns.Contains("*") || inPivot)) .SelectMany(cte => cte.LinkedTables) .Distinct()); - matches.AddRange(cteMatches.Where(cte => cte.LinkedTables.Any(lt => - lt.PossibleSelectColumns.Contains(column.Name.ToLowerInvariant()) - || lt.PossibleSelectColumns.Contains("*") - || inPivot)) - .SelectMany(cte => cte.LinkedTables) - .Distinct()); - } + if (!linkedMatches.Any()) + { + matches.AddRange(cteMatches.Where(cte => cte.LinkedTables.Any(lt => + lt.PossibleSelectColumns.Contains(column.Name.ToLowerInvariant()) + || lt.PossibleSelectColumns.Contains("*") + || inPivot)) + .SelectMany(cte => cte.LinkedTables) + .Distinct()); + } + } else if (identifiers.Length == 3) { var schemaName = identifiers[0]; @@ -279,9 +289,13 @@ public Column GetColumnFromIdentifiers(string[] identifiers, string alias, IEnum .Select(m => m.Name.ToLowerInvariant()).ToArray(); cteMatches.AddRange(ctes.Where(cte => tableNames.Contains(cte.Name.ToLowerInvariant()))); - if (cteMatches.Any()) + cteMatches = cteMatches.Distinct().ToList(); + linkedMatches = linkedMatches.Distinct().ToList(); + matches = matches.Distinct().ToList(); + + if (cteMatches.Any()) { - column.CteReferences = cteMatches.Select(t => new Cte { Name = t.Name, Alias = t.Alias }).ToArray(); + column.CteReferences = cteMatches.Select(t => new Cte { Name = t.Name }).ToArray(); var cteNames = ctes.Select(cte => cte.Name.ToLowerInvariant()).ToArray(); var reject = matches.Where(m => m.Server == null && m.Database == null && m.Schema == null && cteNames.Contains(m.Name.ToLowerInvariant())); @@ -299,6 +313,14 @@ public Column GetColumnFromIdentifiers(string[] identifiers, string alias, IEnum Dump($"{_pad}> FOUND TABLES (MATCHES: {matches.Count}): [{ string.Join(", ", matches.Select(c => c.FullyQualifiedName)) }]"); if (matches.Count > 1) column.AmbiguousTableReferences = matches; else if (matches.Count == 1) column.AbsoluteTableReference = matches.First(); + else if (cteMatches.Count == 1 && cteMatches.First().LinkedTables.Count == 1) + { + column.AbsoluteTableReference = cteMatches.First().LinkedTables.First(); + } + else if (cteMatches.SelectMany(c => c.LinkedTables).Distinct().Count() == 1) + { + column.AbsoluteTableReference = cteMatches.SelectMany(c => c.LinkedTables).Distinct().First(); + } } var matchingTable = column.AbsoluteTableReference != null @@ -374,15 +396,18 @@ public Tuple, IEnumerable, IEnumerable
> HandleCt } cte.LinkedTables = tables.Distinct().ToList(); + foreach (var linkedTable in cte.LinkedTables) { linkedTable.SelectColumns = columns .Where(c => ReferenceEquals(c.AbsoluteTableReference, linkedTable)).Select(c => c.Name.ToLowerInvariant()) + .Distinct() .ToArray(); linkedTable.PossibleSelectColumns = columns .Where(c => c.AmbiguousTableReferences != null && c.AmbiguousTableReferences.Any(t => ReferenceEquals(t, linkedTable))) .Select(c => c.Name.ToLowerInvariant()) - .ToArray(); + .Distinct() + .ToArray(); } if (cte.LinkedTables.Any()) Dump($"{_pad}FOUND CTE-LINKED TABLES: {cte.Name}, tables: {string.Join(", ", cte.LinkedTables.Select(t => t.FullyQualifiedName))}"); @@ -426,8 +451,8 @@ public Tuple, IEnumerable
> HandleQuery(object obj, Ct tables.AddRange(result.Item2); } } - - var fromClause = Helpers.GetPropertyValueWithName(obj, "FromClause"); + + var fromClause = Helpers.GetPropertyValueWithName(obj, "FromClause"); if (fromClause != null) { var tableReferences = Helpers.GetEnumerablePropertyValueWithName(fromClause, "TableReferences"); @@ -450,7 +475,14 @@ public Tuple, IEnumerable
> HandleQuery(object obj, Ct Dump($"{_pad}PROPERTY [SelectElements]: handling columns..."); foreach (var element in selectElements) { - columns.AddRange(HandleColumns(element, null, tables.ToArray(), parentTables, ctes)); + if (element is SelectScalarExpression selectScalarExpression && selectScalarExpression.Expression is StringLiteral) + { + columns.Add(new Column { Name = selectScalarExpression.ColumnName.Value }); + } + else + { + columns.AddRange(HandleColumns(element, null, tables.ToArray(), parentTables, ctes)); + } } } @@ -557,7 +589,7 @@ public Tuple, IEnumerable
> HandleTables(object obj, Cte[ { tables.Add(GetTableFromReference(obj as NamedTableReference)); } - else if (obj.GetType().IsAssignableFrom(typeof(QueryDerivedTable))) + else if (obj.GetType().IsAssignableFrom(typeof(QueryDerivedTable))) { var derived = obj as QueryDerivedTable; Dump($"{_pad}ANALYZING QueryDerivedTable..."); @@ -566,24 +598,48 @@ public Tuple, IEnumerable
> HandleTables(object obj, Cte[ { Dump($"{_pad}PROPERTY [QueryExpression]: handling..."); var result = HandleQuery(derived.QueryExpression, parentCtes, parentTables); - var cte = new Cte { Name = derived.Alias?.Value, LinkedTables = result.Item2.Distinct().ToList() }; + var cte = new Cte { Name = derived.Alias?.Value, LinkedTables = result.Item2.Distinct().ToList() }; foreach (var linkedTable in cte.LinkedTables) { linkedTable.SelectColumns = result.Item1 .Where(c => ReferenceEquals(c.AbsoluteTableReference, linkedTable)).Select(c => c.Name.ToLowerInvariant()) - .ToArray(); + .Distinct() + .ToArray(); linkedTable.PossibleSelectColumns = result.Item1 .Where(c => c.AmbiguousTableReferences != null && c.AmbiguousTableReferences.Any(t => ReferenceEquals(t, linkedTable))) .Select(c => c.Name.ToLowerInvariant()) - .ToArray(); + .Distinct() + .ToArray(); } if (cte.LinkedTables.Any()) Dump($"{_pad}FOUND DERIVED-LINKED TABLES: {cte.Name}, tables: {string.Join(", ", cte.LinkedTables.Select(t => t.FullyQualifiedName))}"); ctes.Add(cte); } } - else + else if (obj.GetType().IsAssignableFrom(typeof(UnpivotedTableReference))) + { + if (obj is UnpivotedTableReference unpivoted) + { + var linkedTables = HandleTables(unpivoted.TableReference, parentCtes, parentTables); + var cteLinkedTables = linkedTables.Item1.SelectMany(c => c.LinkedTables).Concat(linkedTables.Item2); + + ctes.AddRange(linkedTables.Item1); + ctes.Add(new Cte { Name = unpivoted.Alias.Value, LinkedTables = cteLinkedTables.ToList() }); + } + } + else if (obj.GetType().IsAssignableFrom(typeof(PivotedTableReference))) + { + if (obj is PivotedTableReference pivoted) + { + var linkedTables = HandleTables(pivoted.TableReference, parentCtes, parentTables); + var cteLinkedTables = linkedTables.Item1.SelectMany(c => c.LinkedTables).Concat(linkedTables.Item2); + + ctes.AddRange(linkedTables.Item1); + ctes.Add(new Cte { Name = pivoted.Alias.Value, LinkedTables = cteLinkedTables.ToList() }); + } + } + else { var references = Helpers.GetPropertiesWithNames(obj, "FirstTableReference", "SecondTableReference", "TableReference", "Join").ToArray(); if (references.Any()) _level++; @@ -627,10 +683,6 @@ public IEnumerable HandleColumns(object obj, string alias, Table[] table handleColumns.AddRange(HandleProperties(obj, alias, tables, parentTables, ctes)); } } - else if (obj.GetType().IsAssignableFrom(typeof(ColumnReferenceExpression))) - { - handleColumns.Add(GetColumnFromReference(obj as ColumnReferenceExpression, alias, tables, parentTables, ctes)); - } else if (obj.GetType().IsAssignableFrom(typeof(SelectStarExpression))) { handleColumns.Add(GetColumnFromStarReference(obj as SelectStarExpression, tables, parentTables, ctes)); @@ -639,13 +691,35 @@ public IEnumerable HandleColumns(object obj, string alias, Table[] table { if (obj is PivotedTableReference pivotedTableReference) { - handleColumns.Add(GetColumnFromReference(pivotedTableReference.PivotColumn, null, tables, parentTables, ctes, true)); - if (pivotedTableReference.ValueColumns != null) + string tableName = null; + string tableAlias = null; + + if (pivotedTableReference.TableReference is NamedTableReference namedTableReference) + { + tableName = namedTableReference.SchemaObject.BaseIdentifier.Value; + tableAlias = namedTableReference.Alias?.Value; + } + if (pivotedTableReference.TableReference is QueryDerivedTable queryDerivedTable) + { + tableAlias = queryDerivedTable.Alias?.Value; + } + + handleColumns.Add(GetColumnFromReference(pivotedTableReference.PivotColumn, null, tables, parentTables, ctes, true)); + + if (pivotedTableReference.PivotColumn != null) + { + var identifiers = new[] { tableAlias ?? tableName }.Concat(pivotedTableReference.PivotColumn.MultiPartIdentifier.Identifiers.Select(i => i.Value)).ToArray(); + var column = GetColumnFromIdentifiers(identifiers, null, tables, parentTables, ctes, true); + handleColumns.Add(column); + } + + if (pivotedTableReference.ValueColumns != null) { foreach (var valueColumn in pivotedTableReference.ValueColumns) - { - handleColumns.Add(GetColumnFromReference(valueColumn, null, tables, parentTables, ctes, true)); - } + { + var column = GetColumnFromReference(valueColumn, null, tables, parentTables, ctes, true); + handleColumns.Add(column); + } } } @@ -654,20 +728,54 @@ public IEnumerable HandleColumns(object obj, string alias, Table[] table else if (obj.GetType().IsAssignableFrom(typeof(UnpivotedTableReference))) { if (obj is UnpivotedTableReference unpivotedTableReference) - { - if (unpivotedTableReference.PivotColumn?.Value != null) - { - handleColumns.Add(GetColumnFromIdentifiers(new[] { unpivotedTableReference.PivotColumn.Value }, null, tables, parentTables, ctes, true)); - } - if (unpivotedTableReference.ValueColumn?.Value != null) - { - handleColumns.Add(GetColumnFromIdentifiers(new[] { unpivotedTableReference.ValueColumn.Value }, null, tables, parentTables, ctes, true)); - } + { + string tableName = null; + string tableAlias = null; + + if (unpivotedTableReference.TableReference is NamedTableReference namedTableReference) + { + tableName = namedTableReference.SchemaObject.BaseIdentifier.Value; + tableAlias = namedTableReference.Alias?.Value; + } + if (unpivotedTableReference.TableReference is QueryDerivedTable queryDerivedTable) + { + tableAlias = queryDerivedTable.Alias?.Value; + } + + if (unpivotedTableReference.PivotColumn?.Value != null) + { + var column = GetColumnFromIdentifiers(new[] { tableAlias ?? tableName, unpivotedTableReference.PivotColumn.Value }, null, tables, parentTables, ctes, true); + column.IsPivotGeneratedColumn = true; + Dump($"{_pad}* UNPIVOT GENERATED COLUMN"); + column.AbsoluteTableReference.SelectColumns = column.AbsoluteTableReference.SelectColumns.Concat(new[] { column.Name.ToLowerInvariant() }).Distinct().ToArray(); + handleColumns.Add(column); + } + if (unpivotedTableReference.ValueColumn?.Value != null) + { + var column = GetColumnFromIdentifiers(new[] { tableAlias ?? tableName, unpivotedTableReference.ValueColumn.Value }, null, tables, parentTables, ctes, true); + column.IsPivotGeneratedColumn = true; + Dump($"{_pad}* UNPIVOT GENERATED COLUMN"); + column.AbsoluteTableReference.SelectColumns = column.AbsoluteTableReference.SelectColumns.Concat(new[] { column.Name.ToLowerInvariant() }).Distinct().ToArray(); + handleColumns.Add(column); + } + if (unpivotedTableReference.InColumns?.Count > 0) + { + foreach (var inColumn in unpivotedTableReference.InColumns) + { + var identifiers = new [] { tableAlias ?? tableName }.Concat(inColumn.MultiPartIdentifier.Identifiers.Select(i => i.Value)).ToArray(); + + handleColumns.Add(GetColumnFromIdentifiers(identifiers, null, tables, parentTables, ctes, true)); + } + } } handleColumns.AddRange(HandleProperties(obj, alias, tables, parentTables, ctes)); } - else if (obj is QueryExpression) + else if (obj.GetType().IsAssignableFrom(typeof(ColumnReferenceExpression))) + { + handleColumns.Add(GetColumnFromReference(obj as ColumnReferenceExpression, alias, tables, parentTables, ctes)); + } + else if (obj is QueryExpression) { Dump($"{_pad}HANDLING QueryExpression..."); handleColumns.AddRange(HandleQuery(obj, ctes, tables).Item1); @@ -695,7 +803,7 @@ public IEnumerable HandleProperties(object obj, string alias, Table[] ta if (subQuery != null) { Dump($"{_pad}Is SubQuery..."); - var columns = TraverseObject(subQuery, ctes, tables.ToArray()); + var columns = TraverseObject(subQuery, ctes, tables); handleColumns.AddRange(columns); } @@ -706,12 +814,12 @@ public IEnumerable HandleProperties(object obj, string alias, Table[] ta var properties = new[] { - "FirstQueryExpression", "SecondQueryExpression", "FirstTableReference", "SecondTableReference", "TableReference", "Join", - "FirstExpression", "SecondExpression", "WhenExpression", "ThenExpression", "ElseExpression", "Expression", "Expressions", - "FirstExpression", "SecondExpression", "ThirdExpression", "QueryExpression", "Parameter", "SearchCondition", "Parameters", - "WhenClauses", "OrderByClause", "OrderByElements", "OverClause", "InputExpression", "WithinGroupClause", "Partitions", - "WindowFrameClause", "WindowDelimiter", "OffsetValue", "Top", "Bottom", "Predicate", "Values" - }; + "FirstQueryExpression", "SecondQueryExpression", "FirstTableReference", "SecondTableReference", "TableReference", "Join", + "FirstExpression", "SecondExpression", "WhenExpression", "ThenExpression", "ElseExpression", "Expression", "Expressions", + "FirstExpression", "SecondExpression", "ThirdExpression", "QueryExpression", "Parameter", "SearchCondition", "Parameters", + "WhenClauses", "OrderByClause", "OrderByElements", "OverClause", "InputExpression", "WithinGroupClause", "Partitions", + "WindowFrameClause", "WindowDelimiter", "OffsetValue", "Top", "Bottom", "Predicate", "Values" + }; var references = Helpers.GetPropertiesWithNames(obj, properties.Except(skipProperties).ToArray()).ToArray(); diff --git a/SqlParser.linq b/SqlParser.linq index e9554a9..bc61d4b 100644 --- a/SqlParser.linq +++ b/SqlParser.linq @@ -1,25 +1,49 @@ - C:\SC\SQL Parser\ColumnExtractor\bin\Release\ColumnExtractor.dll + C:\Source\Catalyst.SqlUtilities\ColumnExtractor\bin\Debug\ColumnExtractor.dll + C:\Source\Catalyst.SqlUtilities\ColumnExtractor\bin\Debug\Microsoft.SqlServer.TransactSql.ScriptDom.dll ColumnExtractor Microsoft.SqlServer.TransactSql.ScriptDom + ColumnExtractor.Traverse + ColumnExtractor.Models void Main() { - var sql = @"SELECT - DATEADD(D,Something,CAST('2011-09-19 16:52:00.000' AS DATE)) - FROM SAM.InfectiousDisease.CAUTI26MetricCensusPatientDay a - INNER JOIN Foo f ON f.id = a.id + var sql = @" +SELECT + year(invoiceDate) as [year],left(datename(month,invoicedate),3)as [month], + InvoiceAmount as Amount + FROM Invoice +PIVOT +( + SUM(Amount) + FOR [month] IN (jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec) +)AS pvt "; - //sql.Dump("SQL"); - var parser = new Parser(true, true, true); + Dump(sql); + var parser = new Parser(true, true, true, false); + var traverser = new Traverser(true); var results = parser.GetTables(sql); parser.GetErrors(sql).Dump("ERRORS"); - //parser.GetStatements(sql).Dump("STATEMENTS"); + parser.GetStatements(sql).Dump("STATEMENTS"); results.Dump("RESULTS"); results.SelectMany(r => r.Columns).Select(c => c.FullyQualifiedNM).Dump("COLUMNS"); results.SelectMany(r => r.PossibleColumns).Select(c => c.FullyQualifiedNM).Distinct().Dump("UNKNOWN COLUMNS"); +} + +private void Dump(string sql) +{ + var parser = new TSql140Parser(true); + var parsedData = new ParsedData(); + + using (var reader = new StringReader(sql)) + { + var result = parser.Parse(reader, out var errors); + var traverser = new Traverser(false); + var columnData = traverser.TraverseObject(result, null).Distinct().ToArray(); + columnData.Dump(); + } } \ No newline at end of file diff --git a/UnitTests/Tests.cs b/UnitTests/Tests.cs index 19172f8..b480fae 100644 --- a/UnitTests/Tests.cs +++ b/UnitTests/Tests.cs @@ -22,179 +22,16 @@ public void SmokeTest() public void Query1() { var sql = @" -WITH -LinkMeasureElementCTE AS ( - SELECT * - FROM SAM.Pneumonia.QVExtElementBASE -), + WITH + LinkMeasureElementCTE AS (SELECT * FROM SAM.Pneumonia.QVExtElementBASE), + ConfigReplaceCTE AS (SELECT [ExpressionCD] = 'Base'), + StandardExpressionCTE AS (SELECT ElementCD FROM LinkMeasureElementCTE CROSS JOIN ConfigReplaceCTE c) -ConfigReplaceCTE AS ( - SELECT - [ExpressionCD] = 'Base', - [ReplaceSetAnalysisWithSTR] = '', - [ReplaceAggrWithSTR] = '@(=%DateCycleGroupByCOL)' - UNION ALL - SELECT - [ExpressionCD] = 'Total', - [ReplaceSetAnalysisWithSTR] = 'TOTAL ', - [ReplaceAggrWithSTR] = '@(=%DateCycleGroupByCOL)' - UNION ALL - SELECT - [ExpressionCD] = 'PeriodCurrent', - [ReplaceSetAnalysisWithSTR] = '{<@(=@(xPeriodCurrent))>}', - [ReplaceAggrWithSTR] = '@(=%MeasureRangeGroupByCOL)' - UNION ALL - SELECT - [ExpressionCD] = 'PeriodPrior', - [ReplaceSetAnalysisWithSTR] = '{<@(=@(xPeriodPrior))>}', - [ReplaceAggrWithSTR] = '@(=%MeasureRangeGroupByCOL)' -), - - - -/** add standard set analysis expressions **/ -StandardExpressionCTE AS ( - SELECT - MeasureID - ,ElementCD - ,c.ExpressionCD AS ExpressionCD - ,REPLACE(REPLACE(ExpressionSTR,SetAnalysisSTR,c.ReplaceSetAnalysisWithSTR),AggrSTR,c.ReplaceAggrWithSTR) AS ExpressionSTR - FROM LinkMeasureElementCTE - CROSS JOIN ConfigReplaceCTE c - - UNION ALL - SELECT - MeasureID - ,ElementCD - ,'BaseChng' AS ExpressionCD - ,'NUM(@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={Base}>}%ExpressionSTR))-@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={Total}>}%ExpressionSTR)),chr(9650)&'+CHAR(39)+MeasureFormatCD+';'+CHAR(39)+'&chr(9660)&'+CHAR(39)+MeasureFormatCD+CHAR(39)+')' AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION ALL - SELECT - MeasureID - ,ElementCD - ,'BasePcntChng' AS ExpressionCD - ,'NUM((@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={Base}>}%ExpressionSTR))-@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={Total}>}%ExpressionSTR)))/(@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={Base}>}%ExpressionSTR))),'+CHAR(39)+'#,##0%'+CHAR(39)+')' AS ExpressionSTR - FROM LinkMeasureElementCTE - - UNION ALL - SELECT - MeasureID - ,ElementCD - ,'PeriodChng' AS ExpressionCD - ,'NUM(@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={PeriodCurrent}>}%ExpressionSTR))-@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={PeriodPrior}>}%ExpressionSTR)),chr(9650)&'+CHAR(39)+MeasureFormatCD+';'+CHAR(39)+'&chr(9660)&'+CHAR(39)+MeasureFormatCD+CHAR(39)+')' AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION ALL - SELECT - MeasureID - ,ElementCD - ,'PeriodPcntChng' AS ExpressionCD - ,'NUM((@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={PeriodCurrent}>}%ExpressionSTR))-@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={PeriodPrior}>}%ExpressionSTR)))/(@(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={PeriodCurrent}>}%ExpressionSTR))),'+CHAR(39)+'#,##0%'+CHAR(39)+')' AS ExpressionSTR - FROM LinkMeasureElementCTE -), - -/** add additional elements for informational purposes **/ -InfoExpressionCTE AS ( - SELECT - MeasureID - ,'LABEL' AS ElementCD - ,'Base' AS ExpressionCD - ,CHAR(39)+MeasureNM+CHAR(39) AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION - SELECT - MeasureID - ,'UNIT' AS ElementCD - ,'Base' AS ExpressionCD - ,CHAR(39)+MeasureUnitDSC+CHAR(39) AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION - SELECT - MeasureID - ,'UNIT_NUMER' AS ElementCD - ,'Base' AS ExpressionCD - ,CHAR(39)+NumUnitDSC+CHAR(39) AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION - SELECT - MeasureID - ,'UNIT_DENOM' AS ElementCD - ,'Base' AS ExpressionCD - ,CHAR(39)+DenomUnitDSC+CHAR(39) AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION - SELECT - MeasureID - ,'ICON' AS ElementCD - ,'Base' AS ExpressionCD - ,CHAR(39)+'qmem://ImageID/'+ISNULL(MeasureIconNM,'')+CHAR(39) AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION - SELECT - MeasureID - ,'ICON_GHOST' AS ElementCD - ,'Base' AS ExpressionCD - ,CHAR(39)+'qmem://ImageID/'+ISNULL(MeasureIconNM+'_ghost','')+CHAR(39) AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION - SELECT - MeasureID - ,'ICON_TYPE' AS ElementCD - ,'Base' AS ExpressionCD - ,CASE MeasureTypeDSC - WHEN 'Outcome' THEN CHAR(39)+'qmem://ImageID/outcome_rounded'+CHAR(39) - WHEN 'Process' THEN CHAR(39)+'qmem://ImageID/process_rounded'+CHAR(39) - END AS ExpressionSTR - FROM LinkMeasureElementCTE - UNION - SELECT - MeasureID - ,'TYPE' AS ElementCD - ,'Base' AS ExpressionCD - ,CHAR(39)+MeasureTypeDSC+CHAR(39) AS ExpressionSTR - FROM LinkMeasureElementCTE - - UNION - SELECT - MeasureID - ,ElementCD - ,'BaseChng_Color' AS ExpressionCD - ,CASE - WHEN MeasureDirectionCD = 'n/a' - THEN 'none' - WHEN MeasureDirectionCD = 'Up' - THEN 'IF( @(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={BaseChng}>}%ExpressionSTR)) >= 0,''green'',''red'')' - WHEN MeasureDirectionCD = 'Down' - THEN 'IF( @(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={BaseChng}>}%ExpressionSTR)) > 0,''red'',''green'')' - END AS ExpressionSTR - FROM LinkMeasureElementCTE - - UNION - SELECT - MeasureID - ,ElementCD - ,'PeriodChng_Color' AS ExpressionCD - ,CASE - WHEN MeasureDirectionCD = 'n/a' - THEN 'none' - WHEN MeasureDirectionCD = 'Up' - THEN 'IF( @(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={PeriodChng}>}%ExpressionSTR)) >= 0,''green'',''red'')' - WHEN MeasureDirectionCD = 'Down' - THEN 'IF( @(=ONLY({<%MeasureID={'+CAST(MeasureID AS VARCHAR)+'},%ElementCD={'+ElementCD+'},%ExpressionCD={PeriodChng}>}%ExpressionSTR)) > 0,''red'',''green'')' - END AS ExpressionSTR - FROM LinkMeasureElementCTE -) - -/** union all the magic together **/ -SELECT * -FROM StandardExpressionCTE -UNION ALL -SELECT * -FROM InfoExpressionCTE"; + SELECT * FROM StandardExpressionCTE"; var parser = new Parser(false, true, true); var results = parser.GetTables(sql); - Assert.AreEqual(14, results.SelectMany(r => r.Columns).Count()); + Assert.AreEqual(2, results.SelectMany(r => r.Columns).Count()); } [Test] @@ -435,7 +272,7 @@ ICDDiagnosisCD FOR ICDDiagnosisCodes IN ( var parser = new Parser(); var results = parser.GetTables(sql); - Assert.AreEqual(2, results.SelectMany(r => r.Columns).Count()); + Assert.AreEqual(8, results.SelectMany(r => r.Columns).Count()); } [Test] @@ -548,5 +385,57 @@ public void DatePartUnitIgnored() Assert.AreEqual(1, results.SelectMany(r => r.Columns).Count()); } - } + + [Test] + public void UnpivotColumns() + { + var sql = @"SELECT pv.[Value] + FROM Table1 AS t1 + UNPIVOT + ( + [Value] + FOR [Observed] IN([Column1],[Column2]) + ) AS pv"; + var parser = new Parser(); + var results = parser.GetTables(sql); + + Assert.AreEqual(2, results.SelectMany(r => r.Columns).Count()); + } + + [Test] + public void UnpivotWithJoinColumns() + { + var sql = @"SELECT unp.ColumnNames, unp.ColumnValues, unp.Value1, t2.Category1, t2.Value1 AS t2Value1 FROM ( + SELECT + t1.Item1 + ,t1.Item2 + ,t1.Value1 + FROM table1 AS t1 + ) AS a + UNPIVOT (ColumnValues FOR ColumnNames IN (Item1,Item2)) AS unp + INNER JOIN table2 AS t2 ON t2.[ItemNM] = unp.[ColumnValues]"; + var parser = new Parser(); + var results = parser.GetTables(sql); + + Assert.AreEqual(6, results.SelectMany(r => r.Columns).Count()); + } + + [Test] + public void UnpivotWithUnAliasedColumns() + { + var sql = @"SELECT ColumnNames, ColumnValues, unp.Value1, t2.Category1, t2.Value1 AS t2Value1 FROM ( + SELECT + t1.Item1 + ,t1.Item2 + ,t1.Value1 + FROM table1 AS t1 + ) AS a + UNPIVOT (ColumnValues FOR ColumnNames IN (Item1,Item2)) AS unp + INNER JOIN table2 AS t2 ON t2.[ItemNM] = unp.[ColumnValues]"; + var parser = new Parser(); + var results = parser.GetTables(sql); + + Assert.AreEqual(6, results.SelectMany(r => r.Columns).Count()); + } + } }