diff --git a/samples/features/sql-clr/TransitiveClosure/.gitignore b/samples/features/sql-clr/TransitiveClosure/.gitignore new file mode 100644 index 00000000..407d0cc4 --- /dev/null +++ b/samples/features/sql-clr/TransitiveClosure/.gitignore @@ -0,0 +1,7 @@ +*.cproj.user +.vs/* +.vscode/* +bin/* +obj/* +Properties/PublishProfiles/* +TransitiveClosure.sql \ No newline at end of file diff --git a/samples/features/sql-clr/TransitiveClosure/Properties/AssemblyInfo.cs b/samples/features/sql-clr/TransitiveClosure/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..09186f99 --- /dev/null +++ b/samples/features/sql-clr/TransitiveClosure/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TransitiveClosureAggregatorLibrary")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TransitiveClosureAggregatorLibrary")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("097ef341-926d-4dd2-a434-08e9980d6089")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/features/sql-clr/TransitiveClosure/TransitiveClosure.tt b/samples/features/sql-clr/TransitiveClosure/TransitiveClosure.tt new file mode 100644 index 00000000..96c0308d --- /dev/null +++ b/samples/features/sql-clr/TransitiveClosure/TransitiveClosure.tt @@ -0,0 +1,20 @@ +<#@output extension=".sql"#> +<#@ template language="C#" hostspecific="True" #> + +DROP AGGREGATE IF EXISTS TCC.CLUSTERING; +GO + +--Drop the assembly if it already exists +DROP ASSEMBLY IF EXISTS TransitiveClosure; +GO + +--Create the assembly +CREATE ASSEMBLY TransitiveClosure FROM '<#= this.Host.ResolvePath("bin\\Release\\TransitiveClosureAggregatorLibrary.dll") #>' WITH PERMISSION_SET = SAFE; +GO + +CREATE SCHEMA TCC; +GO + +CREATE AGGREGATE TCC.CLUSTERING(@id1 INT, @id2 INT) +RETURNS NVARCHAR(MAX) +EXTERNAL NAME TransitiveClosure.[TransitiveClosure.Aggregate]; \ No newline at end of file diff --git a/samples/features/sql-clr/TransitiveClosure/TransitiveClosureAggregate.cs b/samples/features/sql-clr/TransitiveClosure/TransitiveClosureAggregate.cs new file mode 100644 index 00000000..20aa3491 --- /dev/null +++ b/samples/features/sql-clr/TransitiveClosure/TransitiveClosureAggregate.cs @@ -0,0 +1,321 @@ +using System; +using System.IO; +using System.Data.SqlTypes; +using System.Text; +using Microsoft.SqlServer.Server; +using System.Collections.Generic; +using System.Collections; + +namespace TransitiveClosure +{ + /// + /// Class that represents a group of numbers in the same cluster. + /// + public class Group: IEnumerable + { + private int? _groupRoot = null; + + private Dictionary _group = new Dictionary(); + + public Dictionary.KeyCollection Elements => _group.Keys; + + public int Count => _group.Keys.Count; + + public bool ContainsElement(int element) + { + return _group.ContainsKey(element); + } + + /// + /// Adds a pair of numbers to a group. + /// + /// + /// + public void AddUnique(int from, int to) + { + if (_groupRoot == null) _groupRoot = from; + this.AddIfNotExists(from); + this.AddIfNotExists(to); + } + /// + /// Adds the element into the current group. + /// + /// The number that should be added. + public void Add(int element) + { + if (_groupRoot == null) _groupRoot = element; + _group.Add(element, true); + } + + /// + /// Adds the element to a group if it is not already there. + /// + /// + public void AddIfNotExists(int element) + { + if (!_group.ContainsKey(element)) + { + _group.Add(element, true); + } + } + + public void MergeWith(Group source) + { + foreach (var e in source.Elements) + { + this.AddIfNotExists(e); + } + } + + public IEnumerator GetEnumerator() + { + foreach(var e in _group.Keys) + { + yield return e; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)GetEnumerator(); + } + + public override string ToString() + { + return string.Format($"[{_groupRoot.Value}]"); + } + } + + public class GroupSet: IEnumerable + { + private List _groupSet = new List(); + private Dictionary _numbers = new Dictionary(); + private int _merges = 0; + + public int Groups => _groupSet.Count; + + public int Numbers => _numbers.Count; + + public int Merges => _merges; + + public void Add(Group group) + { + _groupSet.Add(group); + + foreach(int e in group) + { + if (!_numbers.ContainsKey(e)) + _numbers.Add(e, group); + else + throw new ApplicationException("Element is already assigned to a group"); + } + } + + public List FindInGroups(int from, int to) + { + var result = new List(); + + if (_numbers.ContainsKey(from)) result.Add(_numbers[from]); + if (_numbers.ContainsKey(to)) result.Add(_numbers[to]); + + return result; + } + + public void AddPair(int from, int to) + { + //Find if the inputValue is already in a group + var foundInGroups = FindInGroups(from, to); + + // no item matches: create a new group and add both the values to it + if (foundInGroups.Count == 0) + { + var ng = new Group(); + ng.AddIfNotExists(from); + ng.AddIfNotExists(to); + + _groupSet.Add(ng); + + if (!_numbers.ContainsKey(from)) _numbers.Add(from, ng); + if (!_numbers.ContainsKey(to)) _numbers.Add(to, ng); + } + + // one item match, add the related item to the same group + if (foundInGroups.Count == 1) + { + var g = foundInGroups[0]; + g.AddUnique(from, to); + + if (!_numbers.ContainsKey(from)) _numbers.Add(from, g); else _numbers[from] = g; + if (!_numbers.ContainsKey(to)) _numbers.Add(to, g); else _numbers[to] = g; + } + + // if there is a match for both items but in two different groups + // merge them into just one group and delete the other + if (foundInGroups.Count == 2) + { + var g1 = foundInGroups[0]; + var g2 = foundInGroups[1]; + + if (g1 == g2) return; + + // Always move the smaller group + if (g2.Count > g1.Count) + { + var t = g1; + g1 = g2; + g2 = t; + } + + if (!_numbers.ContainsKey(from)) _numbers.Add(from, g1); else _numbers[from] = g1; + if (!_numbers.ContainsKey(to)) _numbers.Add(to, g1); else _numbers[to] = g1; + + g1.MergeWith(g2); + + foreach(var e in g2) + { + if (!_numbers.ContainsKey(e)) _numbers.Add(e, g1); else _numbers[e] = g1; + } + + _merges += 1; + + _groupSet.Remove(g2); + } + } + + public IEnumerator GetEnumerator() + { + foreach(var g in _groupSet) + { + yield return g; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)GetEnumerator(); + } + } + + /// + /// Aggregate that takes a pair of numbers that represents an edge in some graph/relation. + /// As an output, returns groups of reachable edges, for example: + /// { + /// "0":[1,2,3,4], + /// "1":[5,6,7], + /// "2":[8,9] + /// } + /// + [Serializable] + [SqlUserDefinedAggregateAttribute(Format.UserDefined, MaxByteSize = -1)] + public class Aggregate : IBinarySerialize + { + private GroupSet _groupSet; + + public int Groups => _groupSet.Groups; + + public int Numbers => _groupSet.Numbers; + + public int Merges => _groupSet.Merges; + + public void Init() + { + _groupSet = new GroupSet(); + } + + public void Accumulate(int inputValue1, int inputValue2) + { + _groupSet.AddPair(inputValue1, inputValue2); + } + + public void Merge(Aggregate value) + { + foreach (var g in value._groupSet) + { + int? pe = null; + foreach (var ce in g) + { + if (pe.HasValue) + { + this.Accumulate(pe.Value, ce); + } + pe = ce; + } + } + } + + public SqlString Terminate() + { + return this.ToString(); + } + + public override string ToString() + { + int c = 0; + StringBuilder sb = new StringBuilder(); + sb.Append("{"); + foreach (var g in this._groupSet) + { + sb.Append("\"" + c + "\":["); + + var ea = new int[g.Elements.Count]; + g.Elements.CopyTo(ea, 0); + + sb.Append(string.Join(",", ea)); + + sb.Append("],"); + + c += 1; + } + if (sb.Length > 1) sb.Remove(sb.Length - 1, 1); + sb.Append("}"); + return sb.ToString(); + } + + public void Read(BinaryReader r) + { + if (r == null) throw new ArgumentNullException("r"); + _groupSet = new GroupSet(); + + // Group Count + int g = r.ReadInt32(); + + // For Each Group + for (int j = 0; j < g; j++) + { + var l = new Group(); + + // List Size (or Values Count) + int s = r.ReadInt32(); + + // Read values and put them in the list + for (int i = 0; i < s; i++) + { + l.Add(r.ReadInt32()); + } + + // Add list to dictionary + _groupSet.Add(l); + } + } + + public void Write(BinaryWriter w) + { + if (w == null) throw new ArgumentNullException("w IS NULL"); + + // Group count + w.Write(_groupSet.Groups); + + foreach (var g in _groupSet) + { + // Values Count + w.Write(g.Count); + + // Values + foreach (var e in g) + { + w.Write(e); + } + } + } + } +} \ No newline at end of file diff --git a/samples/features/sql-clr/TransitiveClosure/TransitiveClosureAggregatorLibrary.csproj b/samples/features/sql-clr/TransitiveClosure/TransitiveClosureAggregatorLibrary.csproj new file mode 100644 index 00000000..68a5f113 --- /dev/null +++ b/samples/features/sql-clr/TransitiveClosure/TransitiveClosureAggregatorLibrary.csproj @@ -0,0 +1,73 @@ + + + + + Debug + AnyCPU + {097EF341-926D-4DD2-A434-08E9980D6089} + Library + Properties + TransitiveClosureAggregatorLibrary + TransitiveClosureAggregatorLibrary + v4.5.2 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + + + tcc.pfx + + + + + + + + + + + + + + + + + + True + True + TransitiveClosure.tt + + + TextTemplatingFileGenerator + TransitiveClosure.sql + + + + + + + + + + \ No newline at end of file diff --git a/samples/features/sql-clr/TransitiveClosure/tcc.pfx b/samples/features/sql-clr/TransitiveClosure/tcc.pfx new file mode 100644 index 00000000..12006e67 Binary files /dev/null and b/samples/features/sql-clr/TransitiveClosure/tcc.pfx differ