Skip to content

Add locate support for SQLite #2392

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 6 commits into from
May 25, 2020
Merged
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
1 change: 0 additions & 1 deletion src/NHibernate.Test/Async/Hql/HQLFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,6 @@ public async Task SubStringAsync()
[Test]
public async Task LocateAsync()
{
AssumeFunctionSupported("locate");
using (ISession s = OpenSession())
{
Animal a1 = new Animal("abcdef", 20);
Expand Down
17 changes: 1 addition & 16 deletions src/NHibernate.Test/Async/Linq/FunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
using System.Text.RegularExpressions;
using NHibernate.DomainModel;
using NHibernate.DomainModel.Northwind.Entities;
using NHibernate.Linq;
using NUnit.Framework;
using NHibernate.Linq;

namespace NHibernate.Test.Linq
{
Expand Down Expand Up @@ -195,9 +195,6 @@ where e.FirstName.StartsWith("An")
[Test]
public async Task CharIndexFunctionAsync()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var raw = await ((from e in db.Employees select e.FirstName).ToListAsync());
var expected = raw.Select(x => x.ToLower()).Where(x => x.IndexOf('a') == 0).ToList();

Expand All @@ -214,9 +211,6 @@ where lowerName.IndexOf('a') == 0
[Test]
public async Task CharIndexOffsetNegativeFunctionAsync()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var raw = await ((from e in db.Employees select e.FirstName).ToListAsync());
var expected = raw.Select(x => x.ToLower()).Where(x => x.IndexOf('a', 2) == -1).ToList();

Expand All @@ -233,9 +227,6 @@ where lowerName.IndexOf('a', 2) == -1
[Test]
public async Task IndexOfFunctionExpressionAsync()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var raw = await ((from e in db.Employees select e.FirstName).ToListAsync());
var expected = raw.Select(x => x.ToLower()).Where(x => x.IndexOf("an") == 0).ToList();

Expand All @@ -252,9 +243,6 @@ where lowerName.IndexOf("an") == 0
[Test]
public async Task IndexOfFunctionProjectionAsync()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var raw = await ((from e in db.Employees select e.FirstName).ToListAsync());
var expected = raw.Select(x => x.ToLower()).Where(x => x.Contains("a")).Select(x => x.IndexOf("a", 1)).ToList();

Expand All @@ -271,9 +259,6 @@ where lowerName.Contains("a")
[Test]
public async Task TwoFunctionExpressionAsync()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var query = from e in db.Employees
where e.FirstName.IndexOf("A") == e.BirthDate.Value.Month
select e.FirstName;
Expand Down
1 change: 0 additions & 1 deletion src/NHibernate.Test/Hql/HQLFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,6 @@ public void SubString()
[Test]
public void Locate()
{
AssumeFunctionSupported("locate");
using (ISession s = OpenSession())
{
Animal a1 = new Animal("abcdef", 20);
Expand Down
16 changes: 0 additions & 16 deletions src/NHibernate.Test/Linq/FunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Text.RegularExpressions;
using NHibernate.DomainModel;
using NHibernate.DomainModel.Northwind.Entities;
using NHibernate.Linq;
using NUnit.Framework;

namespace NHibernate.Test.Linq
Expand Down Expand Up @@ -184,9 +183,6 @@ where e.FirstName.StartsWith("An")
[Test]
public void CharIndexFunction()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var raw = (from e in db.Employees select e.FirstName).ToList();
var expected = raw.Select(x => x.ToLower()).Where(x => x.IndexOf('a') == 0).ToList();

Expand All @@ -203,9 +199,6 @@ where lowerName.IndexOf('a') == 0
[Test]
public void CharIndexOffsetNegativeFunction()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var raw = (from e in db.Employees select e.FirstName).ToList();
var expected = raw.Select(x => x.ToLower()).Where(x => x.IndexOf('a', 2) == -1).ToList();

Expand All @@ -222,9 +215,6 @@ where lowerName.IndexOf('a', 2) == -1
[Test]
public void IndexOfFunctionExpression()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var raw = (from e in db.Employees select e.FirstName).ToList();
var expected = raw.Select(x => x.ToLower()).Where(x => x.IndexOf("an") == 0).ToList();

Expand All @@ -241,9 +231,6 @@ where lowerName.IndexOf("an") == 0
[Test]
public void IndexOfFunctionProjection()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var raw = (from e in db.Employees select e.FirstName).ToList();
var expected = raw.Select(x => x.ToLower()).Where(x => x.Contains("a")).Select(x => x.IndexOf("a", 1)).ToList();

Expand All @@ -260,9 +247,6 @@ where lowerName.Contains("a")
[Test]
public void TwoFunctionExpression()
{
if (!TestDialect.SupportsLocate)
Assert.Ignore("Locate function not supported.");

var query = from e in db.Employees
where e.FirstName.IndexOf("A") == e.BirthDate.Value.Month
select e.FirstName;
Expand Down
1 change: 0 additions & 1 deletion src/NHibernate.Test/TestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,6 @@ protected DateTime RoundForDialect(DateTime value)
private static readonly Dictionary<string, HashSet<System.Type>> DialectsNotSupportingStandardFunction =
new Dictionary<string, HashSet<System.Type>>
{
{"locate", new HashSet<System.Type> {typeof (SQLiteDialect)}},
{"bit_length", new HashSet<System.Type> {typeof (SQLiteDialect)}},
{"extract", new HashSet<System.Type> {typeof (SQLiteDialect)}},
{
Expand Down
1 change: 0 additions & 1 deletion src/NHibernate.Test/TestDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public bool NativeGeneratorSupportsBulkInsertion

public virtual bool SupportsOperatorAll => true;
public virtual bool SupportsOperatorSome => true;
public virtual bool SupportsLocate => true;

public virtual bool SupportsFullJoin => true;

Expand Down
5 changes: 0 additions & 5 deletions src/NHibernate.Test/TestDialects/SQLiteTestDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ public override bool SupportsOperatorSome
get { return false; }
}

public override bool SupportsLocate
{
get { return false; }
}

public override bool SupportsFullJoin
{
get { return false; }
Expand Down
72 changes: 72 additions & 0 deletions src/NHibernate/Dialect/SQLiteDialect.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Text;
using NHibernate.Dialect.Function;
using NHibernate.Engine;
using NHibernate.SqlCommand;
using NHibernate.Type;
using NHibernate.Util;

namespace NHibernate.Dialect
Expand Down Expand Up @@ -109,6 +113,7 @@ protected virtual void RegisterFunctions()
RegisterFunction("trim", new AnsiTrimEmulationFunction());
RegisterFunction("replace", new StandardSafeSQLFunction("replace", NHibernateUtil.String, 3));
RegisterFunction("chr", new StandardSQLFunction("char", NHibernateUtil.Character));
RegisterFunction("locate", new LocateFunction());

RegisterFunction("mod", new ModulusFunctionTemplate(false));

Expand Down Expand Up @@ -513,5 +518,72 @@ protected override bool CastingIsRequired(string sqlType)
return true;
}
}

[Serializable]
private class LocateFunction : ISQLFunction, ISQLFunctionExtended
{
// Since v5.3
[Obsolete("Use GetReturnType method instead.")]
public IType ReturnType(IType columnType, IMapping mapping)
Copy link
Member

Choose a reason for hiding this comment

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

Nit (not required to change): it seems better to me to explicitly implement obsolete interface members. So when the member is dropped from the interface, its implementation cannot be forgotten. And it would also hide the obsolete member when manipulating a reference typed as the class.

Copy link
Member Author

@bahusoid bahusoid May 24, 2020

Choose a reason for hiding this comment

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

Ok. Noted for future... But I just copy/pasted it from some other LocateFunction for other dialect.

Copy link
Member

Choose a reason for hiding this comment

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

Which was likely already released, and so had to avoid breaking changes. This one is not released, so the nit.

{
return NHibernateUtil.Int32;
}

/// <inheritdoc />
public IType GetReturnType(IEnumerable<IType> argumentTypes, IMapping mapping, bool throwOnError)
{
#pragma warning disable 618
return ReturnType(argumentTypes.FirstOrDefault(), mapping);
#pragma warning restore 618
Copy link
Member

Choose a reason for hiding this comment

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

Nit continuing: this pattern is used for existing externally visible types, with an overridable obsolete member. In this case, an external derived class overriding the obsolete member would be broken without that pattern.
For a new class, there is no need for it. We could put the implementation directly here, and make the obsolete member call the non-obsolete one instead.

}

/// <inheritdoc />
public IType GetEffectiveReturnType(IEnumerable<IType> argumentTypes, IMapping mapping, bool throwOnError)
{
return GetReturnType(argumentTypes, mapping, throwOnError);
}

/// <inheritdoc />
public string Name => "instr";

public bool HasArguments => true;

public bool HasParenthesesIfNoArguments => true;

public SqlString Render(IList args, ISessionFactoryImplementor factory)
{
if (args.Count != 2 && args.Count != 3)
{
throw new QueryException("'locate' function takes 2 or 3 arguments. Provided count: " + args.Count);
}

if (args.Count == 2)
{
return new SqlString("instr(", args[1], ", ", args[0], ")");
}

var text = args[1];
var value = args[0];
var startIndex = args[2];
//ifnull(
Copy link
Member

@fredericDelaporte fredericDelaporte May 24, 2020

Choose a reason for hiding this comment

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

Other nit: I tend to favor the use of functions which are in the SQL Standard, which is the case of coalesce, supported by SQLite, and which can replace ifNull without changing anything more that the function name. I do not know with certainty if ifNull is in the standard, but I think it is not.
(I have not found any reference about it being in the standard or not. It is supported by MySql, SQLite, Big Query, but not by SQL-Server (which has isNull) nor Oracle (nvl), while they all support coalesce.)

Copy link
Member

Choose a reason for hiding this comment

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

This is a dialect specific function implementation, so using non-standard function should be fine here.

// nullif(
// instr(substr(text, startIndex), value)
// , 0)
// + startIndex -1
//, 0)
return
new SqlString(
"ifnull(nullif(instr(substr(",
text,
", ",
startIndex,
"), ",
value,
"), 0) + ",
startIndex,
" -1, 0)"
);
}
}
}
}