Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ~string.IsNullOrEmpty~ string.Length mapping for SQL CE/MSSQL #4177

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Source/LinqToDB/Linq/Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ public void SetInfo(MappingSchema mappingSchema)

#region string

{ M(() => "".Length ), N(() => L<string,int> ((string obj) => Sql.Length(obj)!.Value)) },
{ M(() => "".Length ), N(() => L<string,int> ((string obj) => Sql.StringLength(obj)!.Value)) },
{ M(() => "".Substring (0) ), N(() => L<string?,int,string?> ((string? obj,int p0) => Sql.Substring(obj, p0 + 1, obj!.Length - p0))) },
{ M(() => "".Substring (0,0) ), N(() => L<string?,int,int,string?> ((string? obj,int p0,int p1) => Sql.Substring(obj, p0 + 1, p1))) },
{ M(() => "".IndexOf ("") ), N(() => L<string,string,int> ((string obj,string p0) => p0.Length == 0 ? 0 : (Sql.CharIndex(p0, obj)! .Value) - 1)) },
Expand Down
48 changes: 44 additions & 4 deletions Source/LinqToDB/Sql/Sql.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;

using JetBrains.Annotations;

using PN = LinqToDB.ProviderName;

// ReSharper disable CheckNamespace
// ReSharper disable RedundantNameQualifier

namespace LinqToDB
{
using Mapping;
using Expressions;
using Linq;
using SqlQuery;
using LinqToDB.Common;
using Mapping;
using SqlQuery;

[PublicAPI]
public static partial class Sql
Expand Down Expand Up @@ -269,7 +271,7 @@ public static Guid NewGuid()

[CLSCompliant(false)]
[Function("Convert", 0, 1, ServerSideOnly = true, IsPure = true, IsNullable = IsNullableType.SameAsSecondParameter)]
[Function(PseudoFunctions.CONVERT, 2, 3, 1, ServerSideOnly = true, IsPure = true, IsNullable = IsNullableType.SameAsSecondParameter, Configuration = ProviderName.ClickHouse)]
[Function(PseudoFunctions.CONVERT, 2, 3, 1, ServerSideOnly = true, IsPure = true, IsNullable = IsNullableType.SameAsSecondParameter, Configuration = PN.ClickHouse)]
public static TTo Convert<TTo,TFrom>(TTo to, TFrom from)
{
return Common.ConvertTo<TTo>.From(from);
Expand All @@ -285,7 +287,7 @@ public static TTo Convert<TTo, TFrom>(TTo to, TFrom from, int format)
// TODO: v5 remove. bltoolkit legacy which duplicates Convert function above (without ServerSideOnly, but it shouldn't matter)
[CLSCompliant(false)]
[Function("Convert", 0, 1, IsPure = true, IsNullable = IsNullableType.SameAsSecondParameter)]
[Function(PseudoFunctions.CONVERT, 2, 3, 1, ServerSideOnly = true, IsPure = true, IsNullable = IsNullableType.SameAsSecondParameter, Configuration = ProviderName.ClickHouse)]
[Function(PseudoFunctions.CONVERT, 2, 3, 1, ServerSideOnly = true, IsPure = true, IsNullable = IsNullableType.SameAsSecondParameter, Configuration = PN.ClickHouse)]
public static TTo Convert2<TTo,TFrom>(TTo to, TFrom from)
{
return Common.ConvertTo<TTo>.From(from);
Expand Down Expand Up @@ -383,6 +385,14 @@ public static TTo From<TFrom>(TFrom obj)

#region String Functions

/// <summary>
/// Returns string length in characters by calling corresponding database function as-is.
/// Could result in unexpected results due to non-standard behavior of some databases.
/// E.g. see LEN function notes for SQL CE or MSSQL databases.
/// For more standard behavior look at <see cref="StringLength(string?)"/> function.
/// </summary>
/// <param name="str">String value.</param>
/// <returns>String length or <c>null</c> for NULL input.</returns>
[Function ( PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.Access, "Len", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.Firebird, "Char_Length", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
Expand All @@ -393,11 +403,41 @@ public static TTo From<TFrom>(TFrom obj)
[Function (PN.Informix, "CHAR_LENGTH", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.ClickHouse, "CHAR_LENGTH", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Expression(PN.DB2LUW, "CHARACTER_LENGTH({0},CODEUNITS32)", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[return: NotNullIfNotNull(nameof(str))]
public static int? Length(string? str)
{
return str?.Length;
}

/// <summary>
/// Returns string length in characters by calling corresponding database function.
/// Compared to <see cref="Length(string?)"/> function, it tries to calculate length in same way as <see cref="string.Length"/> property
/// for databases with non-standard length functions.
/// E.g. see LEN function notes for SQL CE or MSSQL databases.
/// Known issues with empty string:
/// <list type="bullet">
/// <item>Oracle treats '' as NULL and returns NULL instead of 1</item>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"instead of 0"

/// <item>Sybase ASE treats '' as ' ' and returns 1 instead of 1</item>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"instead of 0"

/// </list>
/// </summary>
/// <param name="str">String value.</param>
/// <returns>String length or <c>null</c> for NULL input.</returns>
[Function ( "Length", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.Access, "Len", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.Firebird, "Char_Length", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Expression(PN.SqlServer, "LEN(REPLACE({0},' ','.'))", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Expression(PN.SqlCe, "LEN(REPLACE({0},' ','.'))", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.Sybase, "Len", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.MySql, "Char_Length", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.Informix, "CHAR_LENGTH", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Function (PN.ClickHouse, "CHAR_LENGTH", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[Expression(PN.DB2LUW, "CHARACTER_LENGTH({0},CODEUNITS32)", PreferServerSide = true, IsNullable = IsNullableType.SameAsFirstParameter)]
[return: NotNullIfNotNull(nameof(str))]
public static int? StringLength(string? str)
{
return str?.Length;
}

[Function ( PreferServerSide = true, IsNullable = IsNullableType.IfAnyParameterNullable)]
[Function (PN.Access, "Mid", PreferServerSide = true, IsNullable = IsNullableType.IfAnyParameterNullable)]
[Function (PN.DB2, "Substr", PreferServerSide = true, IsNullable = IsNullableType.IfAnyParameterNullable)]
Expand Down
123 changes: 123 additions & 0 deletions Tests/Linq/Linq/StringFunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1449,6 +1449,129 @@ public void IsNullOrEmpty2([DataSources] string context)
}
}

public sealed class IsNullOrEmptyTable
{
public int Id { get; set; }
public string? Value { get; set; }
}

[Test(Description = "https://github.com/linq2db/linq2db/issues/4138")]
public void IsNullOrEmpty3([DataSources] string context)
{
using var db = GetDataContext(context);
using var t = db.CreateLocalTable(new[]
{
new IsNullOrEmptyTable() { Id = 1, Value = " " },
new IsNullOrEmptyTable() { Id = 2, Value = "" },
});

var results = (from p in t where string.IsNullOrEmpty(p.Value) select p).ToList();

if (context.IsAnyOf(TestProvName.AllSybase))
{
Assert.That(results, Is.Empty);
}
else
{
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0].Id, Is.EqualTo(2));
}
}

[Test(Description = "https://github.com/linq2db/linq2db/issues/4138")]
public void IsNullOrEmptySybase([IncludeDataSources(true, TestProvName.AllSybase)] string context)
{
// sybase doesn't have empty string concept
// empty string replaced with ' '
// https://stackoverflow.com/questions/52284561
using var db = GetDataContext(context);
using var t = db.CreateLocalTable(new[]
{
new IsNullOrEmptyTable() { Id = 1, Value = " " },
new IsNullOrEmptyTable() { Id = 2, Value = "" },
});

var results = (from p in t where string.IsNullOrEmpty(p.Value) select p).ToList();

Assert.That(results, Is.Empty);
}

// Sybase: https://stackoverflow.com/questions/52284561
[Test(Description = "https://github.com/linq2db/linq2db/issues/4138")]
public void StringLength1([DataSources] string context)
{
using var db = GetDataContext(context);
using var t = db.CreateLocalTable(new[]
{
new IsNullOrEmptyTable() { Id = 1, Value = " " },
new IsNullOrEmptyTable() { Id = 2, Value = "" },
});

var results = (from p in t where p.Value!.Length == 0 select p).ToList();

if (context.IsAnyOf(TestProvName.AllOracle, TestProvName.AllSybase))
{
Assert.That(results, Is.Empty);
}
else
{
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0].Id, Is.EqualTo(2));
}
}

// Sybase: https://stackoverflow.com/questions/52284561
[Test(Description = "https://github.com/linq2db/linq2db/issues/4138")]
public void StringLength2([DataSources] string context)
{
using var db = GetDataContext(context);
using var t = db.CreateLocalTable(new[]
{
new IsNullOrEmptyTable() { Id = 1, Value = " " },
new IsNullOrEmptyTable() { Id = 2, Value = "" },
});

var results = (from p in t where p.Value!.Length == 3 select p).ToList();

if (context.IsAnyOf(TestProvName.AllSybase))
{
// sybase:
// - trims trailing whitespaces in storage (1)
// - '' replaced with ' '
Assert.That(results, Is.Empty);
}
else
{
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0].Id, Is.EqualTo(1));
}
}

// Sybase: https://stackoverflow.com/questions/52284561
[Test(Description = "https://github.com/linq2db/linq2db/issues/4138")]
public void StringLength3([DataSources] string context)
{
using var db = GetDataContext(context);
using var t = db.CreateLocalTable(new[]
{
new IsNullOrEmptyTable() { Id = 1, Value = "x " },
new IsNullOrEmptyTable() { Id = 2, Value = "xxxx " },
});

var results = (from p in t where p.Value!.Length == 4 select p).ToList();

Assert.That(results.Count, Is.EqualTo(1));
if (context.IsAnyOf(TestProvName.AllSybase))
{
// Sybase removes spaces on insert (?)
Assert.That(results[0].Id, Is.EqualTo(2));
}
else
{
Assert.That(results[0].Id, Is.EqualTo(1));
}
}

[Table]
sealed class CollatedTable
{
Expand Down
Loading