Skip to content

Commit

Permalink
Merge pull request #517 from MarkMpn/string_split
Browse files Browse the repository at this point in the history
String split
  • Loading branch information
MarkMpn authored Jul 18, 2024
2 parents f3385dc + a385447 commit faf54a0
Show file tree
Hide file tree
Showing 10 changed files with 582 additions and 24 deletions.
Binary file not shown.
3 changes: 3 additions & 0 deletions MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@
<Name>MarkMpn.Sql4Cds.Engine</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Images\StringSplitNode.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PostBuildEvent>copy $(TargetDir)MarkMpn.Sql4Cds.Controls.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds</PostBuildEvent>
Expand Down
30 changes: 30 additions & 0 deletions MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8038,5 +8038,35 @@ FROM account as a
</entity>
</fetch>");
}

[TestMethod]
public void MetadataOuterJoin()
{
var query = @"
SELECT a.entitylogicalname,
a.attributetypename,
a.logicalname,
a.targets,
p.logicalname
FROM metadata.attribute AS a
LEFT OUTER JOIN
metadata.entity AS p
ON a.targets = p.logicalname
WHERE a.entitylogicalname IN ('team')";

var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);

var plans = planBuilder.Build(query, null, out _);

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
var join = AssertNode<HashJoinNode>(select.Source);
Assert.AreEqual(QualifiedJoinType.RightOuter, join.JoinType);
var metadata_p = AssertNode<MetadataQueryNode>(join.LeftSource);
Assert.AreEqual("p", metadata_p.EntityAlias);
var metadata_a = AssertNode<MetadataQueryNode>(join.RightSource);
Assert.AreEqual("a", metadata_a.AttributeAlias);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
<Compile Include="Sql2FetchXmlTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SqlVariantTests.cs" />
<Compile Include="StringSplitTests.cs" />
<Compile Include="StubOptions.cs" />
<Compile Include="StubMessageCache.cs" />
<Compile Include="StubTableSizeCache.cs" />
Expand Down
249 changes: 249 additions & 0 deletions MarkMpn.Sql4Cds.Engine.Tests/StringSplitTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MarkMpn.Sql4Cds.Engine.Tests
{
[TestClass]
public class StringSplitTests : FakeXrmEasyTestsBase
{
[TestMethod]
public void InsufficientParameters()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"SELECT * FROM string_split('hello,world')";

try
{
cmd.ExecuteNonQuery();
Assert.Fail("Expected an exception");
}
catch (Sql4CdsException ex)
{
Assert.AreEqual(313, ex.Number);
}
}
}

[TestMethod]
public void TooManyParameters()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"SELECT * FROM string_split('hello,world', ',', 1, 'test')";

try
{
cmd.ExecuteNonQuery();
Assert.Fail("Expected an exception");
}
catch (Sql4CdsException ex)
{
Assert.AreEqual(8144, ex.Number);
}
}
}

[TestMethod]
public void DefaultsToNoOrdinal()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"SELECT * FROM string_split('hello,world', ',')";

using (var reader = cmd.ExecuteReader())
{
Assert.AreEqual(1, reader.FieldCount);
Assert.AreEqual("value", reader.GetName(0));
Assert.IsTrue(reader.Read());
Assert.AreEqual("hello", reader.GetString(0));
Assert.IsTrue(reader.Read());
Assert.AreEqual("world", reader.GetString(0));
Assert.IsFalse(reader.Read());
}
}
}

[TestMethod]
public void IncludesOrdinal()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"SELECT * FROM string_split('hello,world', ',', 1)";

using (var reader = cmd.ExecuteReader())
{
Assert.AreEqual(2, reader.FieldCount);
Assert.AreEqual("value", reader.GetName(0));
Assert.AreEqual("ordinal", reader.GetName(1));
Assert.IsTrue(reader.Read());
Assert.AreEqual("hello", reader.GetString(0));
Assert.AreEqual(1, reader.GetInt32(1));
Assert.IsTrue(reader.Read());
Assert.AreEqual("world", reader.GetString(0));
Assert.AreEqual(2, reader.GetInt32(1));
Assert.IsFalse(reader.Read());
}
}
}

[TestMethod]
public void InputAndSeparatorCanBeParameters()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"SELECT * FROM string_split(@input, @separator, 1)";
cmd.Parameters.Add(cmd.CreateParameter());
cmd.Parameters.Add(cmd.CreateParameter());

cmd.Parameters[0].ParameterName = "@input";
cmd.Parameters[0].Value = "hello,world";
cmd.Parameters[1].ParameterName = "@separator";
cmd.Parameters[1].Value = ",";

using (var reader = cmd.ExecuteReader())
{
Assert.AreEqual(2, reader.FieldCount);
Assert.AreEqual("value", reader.GetName(0));
Assert.AreEqual("ordinal", reader.GetName(1));
Assert.IsTrue(reader.Read());
Assert.AreEqual("hello", reader.GetString(0));
Assert.AreEqual(1, reader.GetInt32(1));
Assert.IsTrue(reader.Read());
Assert.AreEqual("world", reader.GetString(0));
Assert.AreEqual(2, reader.GetInt32(1));
Assert.IsFalse(reader.Read());
}
}
}

[TestMethod]
public void OrdinalCannotBeParameter()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"SELECT * FROM string_split('hello,world', ',', @ordinal)";
cmd.Parameters.Add(cmd.CreateParameter());
cmd.Parameters[0].ParameterName = "@ordinal";
cmd.Parameters[0].Value = true;

try
{
cmd.ExecuteNonQuery();
Assert.Fail("Expected an exception");
}
catch (Sql4CdsException ex)
{
Assert.AreEqual(8748, ex.Number);
}
}
}

[DataTestMethod]
[DataRow("123", 4199)]
[DataRow("'123'", 8116)]
[DataRow("1.0", 8116)]
public void OrdinalMustBeBit(string ordinal, int expectedError)
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = $"SELECT * FROM string_split('hello,world', ',', {ordinal})";

try
{
cmd.ExecuteNonQuery();
Assert.Fail("Expected an exception");
}
catch (Sql4CdsException ex)
{
Assert.AreEqual(expectedError, ex.Number);
}
}
}

[TestMethod]
public void SeparatorCannotBeNull()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"SELECT * FROM string_split('hello,world', null)";

try
{
cmd.ExecuteNonQuery();
Assert.Fail("Expected an exception");
}
catch (Sql4CdsException ex)
{
Assert.AreEqual(214, ex.Number);
}
}
}

[TestMethod]
public void NullInputGivesEmptyResult()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"SELECT * FROM string_split(null, ',')";

using (var reader = cmd.ExecuteReader())
{
Assert.AreEqual(1, reader.FieldCount);
Assert.AreEqual("value", reader.GetName(0));
Assert.IsFalse(reader.Read());
}
}
}

[TestMethod]
public void CrossApply()
{
using (var con = new Sql4CdsConnection(_localDataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"
select * from (values ('a;b'), ('c;d')) as t1 (col)
cross apply string_split(t1.col, ';', 1) s";

using (var reader = cmd.ExecuteReader())
{
Assert.AreEqual(3, reader.FieldCount);
Assert.AreEqual("col", reader.GetName(0));
Assert.AreEqual("value", reader.GetName(1));
Assert.AreEqual("ordinal", reader.GetName(2));
Assert.IsTrue(reader.Read());
Assert.AreEqual("a;b", reader.GetString(0));
Assert.AreEqual("a", reader.GetString(1));
Assert.AreEqual(1, reader.GetInt32(2));
Assert.IsTrue(reader.Read());
Assert.AreEqual("a;b", reader.GetString(0));
Assert.AreEqual("b", reader.GetString(1));
Assert.AreEqual(2, reader.GetInt32(2));
Assert.IsTrue(reader.Read());
Assert.AreEqual("c;d", reader.GetString(0));
Assert.AreEqual("c", reader.GetString(1));
Assert.AreEqual(1, reader.GetInt32(2));
Assert.IsTrue(reader.Read());
Assert.AreEqual("c;d", reader.GetString(0));
Assert.AreEqual("d", reader.GetString(1));
Assert.AreEqual(2, reader.GetInt32(2));
Assert.IsFalse(reader.Read());
}
}
}
}
}
35 changes: 35 additions & 0 deletions MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ internal static Sql4CdsError InvalidObjectName(SchemaObjectName obj)
return Create(208, obj, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name));
}

internal static Sql4CdsError InvalidObjectName(Identifier obj)
{
var name = obj.ToSql();
return Create(208, obj, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name));
}

internal static Sql4CdsError NonFunctionCalledWithParameters(SchemaObjectName obj)
{
var name = obj.ToSql();
Expand Down Expand Up @@ -277,6 +283,12 @@ internal static Sql4CdsError InsufficientArguments(SchemaObjectName sproc)
return Create(313, sproc, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name));
}

internal static Sql4CdsError InsufficientArguments(Identifier function)
{
var name = function.ToSql();
return Create(313, function, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name));
}

internal static Sql4CdsError TooManyArguments(SchemaObjectName sprocOrFunc, bool isSproc)
{
var name = sprocOrFunc.ToSql();
Expand All @@ -288,6 +300,14 @@ internal static Sql4CdsError TooManyArguments(SchemaObjectName sprocOrFunc, bool
return err;
}

internal static Sql4CdsError TooManyArguments(Identifier function)
{
var name = function.ToSql();
var err = Create(8144, function, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name));

return err;
}

internal static Sql4CdsError NamedParametersRequiredAfter(ExecuteParameter param, int paramIndex)
{
return Create(119, param, (SqlInt32)paramIndex);
Expand Down Expand Up @@ -322,6 +342,11 @@ internal static Sql4CdsError InvalidArgumentType(TSqlFragment fragment, DataType
return Create(8116, fragment, Collation.USEnglish.ToSqlString(GetTypeName(type)), (SqlInt32)paramNum, Collation.USEnglish.ToSqlString(function));
}

internal static Sql4CdsError InvalidArgumentValue(TSqlFragment fragment, int value, int paramNum, string function)
{
return Create(4199, fragment, (SqlInt32)value, (SqlInt32)paramNum, Collation.USEnglish.ToSqlString(function));
}

internal static Sql4CdsError StringTruncation(TSqlFragment fragment, string table, string column, string value)
{
return Create(2628, fragment, (SqlInt32)table.Length, Collation.USEnglish.ToSqlString(table), (SqlInt32)column.Length, Collation.USEnglish.ToSqlString(column), (SqlInt32)value.Length, Collation.USEnglish.ToSqlString(value));
Expand Down Expand Up @@ -745,6 +770,16 @@ internal static Sql4CdsError IncompatibleDataTypesForOperator(TSqlFragment fragm
return Create(402, fragment, Collation.USEnglish.ToSqlString(GetTypeName(type1)), Collation.USEnglish.ToSqlString(GetTypeName(type2)), Collation.USEnglish.ToSqlString(op));
}

internal static Sql4CdsError StringSplitOrdinalRequiresLiteral(TSqlFragment fragment)
{
return Create(8748, fragment);
}

internal static Sql4CdsError InvalidProcedureParameterType(TSqlFragment fragment, string parameter, string type)
{
return Create(214, fragment, parameter, type);
}

private static string GetTypeName(DataTypeReference type)
{
if (type is SqlDataTypeReference sqlType)
Expand Down
7 changes: 5 additions & 2 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashJoinNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,11 @@ public override IDataExecutionPlanNodeInternal FoldQuery(NodeCompilationContext

// Make sure the join keys are not null - the SqlType classes override == to prevent NULL = NULL
// but .Equals used by the hash table allows them to match
LeftSource = AddNotNullFilter(LeftSource, LeftAttribute, context, hints, true);
RightSource = AddNotNullFilter(RightSource, RightAttribute, context, hints, true);
if (JoinType == QualifiedJoinType.Inner || JoinType == QualifiedJoinType.RightOuter)
LeftSource = AddNotNullFilter(LeftSource, LeftAttribute, context, hints, true);

if (JoinType == QualifiedJoinType.Inner || JoinType == QualifiedJoinType.LeftOuter)
RightSource = AddNotNullFilter(RightSource, RightAttribute, context, hints, true);

return this;
}
Expand Down
Loading

0 comments on commit faf54a0

Please sign in to comment.