Skip to content

Commit

Permalink
Added string_split support
Browse files Browse the repository at this point in the history
Fixes #412
  • Loading branch information
MarkMpn committed Jul 18, 2024
1 parent 2cbb845 commit 336a47b
Show file tree
Hide file tree
Showing 8 changed files with 547 additions and 22 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
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
Loading

0 comments on commit 336a47b

Please sign in to comment.