Skip to content

Improvement of string operations within queries #421

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

Merged
merged 10 commits into from
Jan 28, 2025
7 changes: 6 additions & 1 deletion ChangeLog/7.2.0-Beta-2-dev.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
[main] Upgrade hints change names of constructors' string parameters for better understanding of what suppose to be in them.
[main] Upgrade hints change names of constructors' string parameters for better understanding of what suppose to be in them.
[main] Improved string operations Trim/TrimStart/TrimEnd support
[mysql] SqlDml.NullIf function now correctly translated
[mysql] Improved support for string.PadLeft/PadRight opertaions
[sqlite] Fixed string.Lenght translation
[sqlite] Added support for string.PadLeft/PadRight operations
2 changes: 1 addition & 1 deletion Orm/Xtensive.Orm.MySql/Sql.Drivers.MySql/v5_0/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public override void Visit(SqlFunctionCall node)
return;
case SqlFunctionType.PadLeft:
case SqlFunctionType.PadRight:
SqlHelper.GenericPad(node).AcceptVisitor(this);
SqlHelper.GenericPad(node, true).AcceptVisitor(this);
return;
case SqlFunctionType.Rand:
SqlDml.FunctionCall(translator.TranslateToString(SqlFunctionType.Rand)).AcceptVisitor(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ public override void Translate(IOutput output, SqlFunctionType type)
case SqlFunctionType.CurrentUser:
_ = output.Append("CURRENT_USER()"); break;
case SqlFunctionType.SessionUser: _ = output.Append("SESSION_USER()"); break;
case SqlFunctionType.NullIf: _ = output.Append("IFNULL"); break;
//datetime/timespan
case SqlFunctionType.DateTimeTruncate: _ = output.Append("DATE"); break;
case SqlFunctionType.CurrentDate: _ = output.Append("CURDATE()"); break;
Expand Down
39 changes: 38 additions & 1 deletion Orm/Xtensive.Orm.Sqlite/Sql.Drivers.Sqlite/v3/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,11 @@ public override void Visit(SqlFunctionCall node)
var arguments = node.Arguments;
switch (node.FunctionType) {
case SqlFunctionType.CharLength:
(SqlDml.FunctionCall("LENGTH", arguments) / 2).AcceptVisitor(this);
SqlDml.FunctionCall("LENGTH", arguments).AcceptVisitor(this);
return;
case SqlFunctionType.PadLeft:
case SqlFunctionType.PadRight:
Visit(EmulateLpadRpad(arguments, node.FunctionType is SqlFunctionType.PadLeft));
return;
case SqlFunctionType.Concat:
var nod = arguments[0];
Expand Down Expand Up @@ -631,6 +632,42 @@ private static SqlDateTimePart ConvertDateTimeOffsetPartToDateTimePart(SqlDateTi
};
}

private static SqlCase EmulateLpadRpad(IReadOnlyList<SqlExpression> arguments, bool isLpad)
{
var operand = arguments[0];
var charcount = arguments[1];
if (charcount is not SqlLiteral<int> intWidth) {
// Since we emulate function with contatination, we need to know total width
// to calculate prefix
throw SqlHelper.NotSupported("PadLeft/PadRight with expressions as total width.");
}
var totalWidth = intWidth.Value;

var padChar = arguments switch {
_ when arguments.Count == 3 && arguments[2] is SqlLiteral<char> charLiteral => charLiteral.Value,
_ when arguments.Count == 2 => ' ',
_ => throw new NotSupportedException()
};

var paddingString = SqlDml.Literal(new string(Enumerable.Repeat(padChar, intWidth.Value).ToArray()));

var padExpression = isLpad
? SqlDml.Substring(
SqlDml.Concat(paddingString, operand),
SqlDml.Literal(-totalWidth - 1),// handles '+1' operation in translation of substring function call
SqlDml.Literal(totalWidth))
: SqlDml.Substring(
SqlDml.Concat(operand, paddingString),
SqlDml.Literal(0), // handles '+1' operation in translation of substring function call
SqlDml.Literal(totalWidth));

var @case = SqlDml.Case();
_ = @case.Add(SqlDml.CharLength(operand) >= charcount, operand);
@case.Else = padExpression;
return @case;
}


// Constructors

/// <param name="driver">The driver.</param>
Expand Down
112 changes: 0 additions & 112 deletions Orm/Xtensive.Orm.Tests/Linq/SelectTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -768,118 +768,6 @@ where invoice.InvoiceId > 0 && invoice.InvoiceId < 50
}
}

[Test]
public void SelectStringIndexerTest()
{
var result =
Session.Query.All<Customer>()
.Select(c => new {
String = c.CustomerId,
Char0 = c.Email[0],
Char1 = c.Email[1],
Char2 = c.Email[2],
Char3 = c.Email[3],
Char4 = c.Email[4],
})
.ToArray()
.OrderBy(item => item.String)
.ToArray();
var expected = Customers
.Select(c => new {
String = c.CustomerId,
Char0 = c.Email[0],
Char1 = c.Email[1],
Char2 = c.Email[2],
Char3 = c.Email[3],
Char4 = c.Email[4],
})
.OrderBy(item => item.String)
.ToArray();
Assert.AreEqual(expected.Length, result.Length);
for (var i = 0; i < expected.Length; i++) {
Assert.AreEqual(expected[0].String, result[0].String);
Assert.AreEqual(expected[0].Char0, result[0].Char0);
Assert.AreEqual(expected[0].Char1, result[0].Char1);
Assert.AreEqual(expected[0].Char2, result[0].Char2);
Assert.AreEqual(expected[0].Char3, result[0].Char3);
Assert.AreEqual(expected[0].Char4, result[0].Char4);
}
}

[Test]
public void SelectIndexOfTest()
{
var _char = 'A';
var result =
Session.Query.All<Customer>()
.Select(c => new {
String = c.FirstName,
IndexOfChar = c.FirstName.IndexOf(_char),
IndexOfCharStart = c.FirstName.IndexOf(_char, 1),
IndexOfCharStartCount = c.FirstName.IndexOf(_char, 1, 1),
IndexOfString = c.FirstName.IndexOf(_char.ToString()),
IndexOfStringStart = c.FirstName.IndexOf(_char.ToString(), 1),
IndexOfStringStartCount = c.FirstName.IndexOf(_char.ToString(), 1, 1)
})
.ToArray()
.OrderBy(item => item.String)
.ToArray();
var expected = Customers
.Select(c => new {
String = c.FirstName,
IndexOfChar = c.FirstName.IndexOf(_char),
IndexOfCharStart = c.FirstName.IndexOf(_char, 1),
IndexOfCharStartCount = c.FirstName.IndexOf(_char, 1, 1),
IndexOfString = c.FirstName.IndexOf(_char.ToString()),
IndexOfStringStart = c.FirstName.IndexOf(_char.ToString(), 1),
IndexOfStringStartCount = c.FirstName.IndexOf(_char.ToString(), 1, 1)
})
.OrderBy(item => item.String)
.ToArray();
Assert.AreEqual(expected.Length, result.Length);
for (var i = 0; i < expected.Length; i++) {
Assert.AreEqual(expected[i].String, result[i].String);
Assert.AreEqual(expected[i].IndexOfChar, result[i].IndexOfChar);
Assert.AreEqual(expected[i].IndexOfCharStart, result[i].IndexOfCharStart);
Assert.AreEqual(expected[i].IndexOfCharStartCount, result[i].IndexOfCharStartCount);
Assert.AreEqual(expected[i].IndexOfString, result[i].IndexOfString);
Assert.AreEqual(expected[i].IndexOfStringStart, result[i].IndexOfStringStart);
Assert.AreEqual(expected[i].IndexOfStringStartCount, result[i].IndexOfStringStartCount);
}
}

[Test]
public void SelectStringContainsTest1()
{
Require.ProviderIs(StorageProvider.Sqlite | StorageProvider.SqlServer | StorageProvider.MySql);
var result =
Session.Query.All<Customer>()
.Where(c => c.FirstName.Contains('L'))
.OrderBy(c => c.CustomerId)
.ToArray();
var expected = Customers
.Where(c => c.FirstName.Contains('L') || c.FirstName.Contains('l'))
.OrderBy(c => c.CustomerId)
.ToArray();
Assert.IsTrue(expected.SequenceEqual(result));
}

[Test]
public void SelectStringContainsTest2()
{
Require.ProviderIsNot(StorageProvider.Sqlite | StorageProvider.SqlServer | StorageProvider.MySql);
var result =
Session.Query.All<Customer>()
.Where(c => c.FirstName.Contains('L'))
.OrderBy(c => c.CustomerId)
.ToArray();
var expected = Customers
.Where(c => c.FirstName.Contains('L'))
.OrderBy(c => c.CustomerId)
.ToArray();
Assert.IsTrue(expected.SequenceEqual(result));
}

[Test]
public void SelectDateTimeTimeSpanTest()
{
Expand Down
Loading