Can't get case-insensitive searches on SQLite - c#

Using SQLite I'm trying to do a case-insensitive search on a string column, however it's only returning results when the case matches the column exactly.
The connection is setup as:
new SQLiteAsyncConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache));
I'm not using any pragma's to configure LIKE case sensitivity
The table is defined as:
[Table($"{nameof(Customer)}s")]
public class Customer
{
[PrimaryKey, AutoIncrement]
public long CustomerId { get; set; }
[Collation("NOCASE")]
public string? BusinessName { get; set; }
[Collation("NOCASE")]
public string? Suburb { get; set; }
// ... etc.
}
(After adding the Collation attribute, I dropped and recreated the table).
There are multiple issues with this.
First, my data has about 9 customers in "BRUNSWICK HEADS" (all uppercase thanks to upstream data out of my control).
Using Linq
var search = "BRUNSWICK";
List<Customer> entities = await _conn.Table<Customer>()
.Where(c =>
c.BusinessName.Contains(search) ||
c.Suburb.Contains(search) ||
c.AddressLine1.Contains(search) ||
c.AddressLine2.Contains(search)
).ToListAsync();
This returns 9 results, but search term and columns are all uppercase.
Mixed case search-terms. These all return 0 results, indicating that the search is case-sensitive.
search = "BrUNSWICK"; // note lower-case 'r'
entities = await _conn.Table<Customer>()
.Where(c =>
c.BusinessName.Contains(search) ||
c.Suburb.Contains(search) ||
c.AddressLine1.Contains(search) ||
c.AddressLine2.Contains(search)
).ToListAsync();
entities = await _conn.Table<Customer>()
.Where(c =>
c.Suburb.Contains(search)
).ToListAsync();
search = "BrUNSWICK HeADS";
entities = await _conn.Table<Customer>()
.Where(c =>
c.BusinessName.Contains(search) ||
c.Suburb.Contains(search) ||
c.AddressLine1.Contains(search) ||
c.AddressLine2.Contains(search)
).ToListAsync();
entities = await _conn.Table<Customer>()
.Where(c =>
c.Suburb.Contains(search)
).ToListAsync();
Perhaps it's just a problem with Linq queries... Let's try SQL expressions:
string query = "SELECT * FROM Customers WHERE Suburb = '?'";
entities = await _conn.QueryAsync<Customer>(query, "BrUNSWICK HeADS");
returns 0 results! (It's a problem with the parameter, not = being case-sensitive)
4.
query = "SELECT * FROM Customers WHERE Suburb LIKE '%?%'";
entities = await _conn.QueryAsync<Customer>(query, "BRUNSWICK");
returns 0 results! (It's a problem with the parameter, not LIKE being case-sensitive)
5.
query = "SELECT * FROM Customers WHERE Suburb = '?'";
entities = await _conn.QueryAsync<Customer>(query, "BRUNSWICK HEADS");
An exact match returns no results...
6.
query = "SELECT * FROM Customers WHERE Suburb = 'BRUNSWICK HEADS'";
entities = await _conn.QueryAsync<Customer>(query);
Returns 9 results - so = is working, but parameters aren't?
7.
query = "SELECT * FROM Customers WHERE Suburb LIKE '%BRUNSWICK%'";
entities = await _conn.QueryAsync<Customer>(query);
Returns 9 results - so LIKE is working, but parameters aren't?
The SQL string statements I can live without, so that's not a focus of this question. The only way to get this working is the following:
search = "BrUNSWICK HeADS".ToLower(); // actually from user input!
entities = await _conn.Table<Customer>()
.Where(c =>
c.BusinessName.ToLower().Contains(search) ||
c.Suburb.ToLower().Contains(search) ||
c.AddressLine1.ToLower().Contains(search) ||
c.AddressLine2.ToLower().Contains(search)
).ToListAsync();
This returns 9 results, but now we can't use column indexes.
Can someone please enlighten me?
There is no mention of COLLATE on TwinCoders site (the SQLiteNetExtensions nuget package website)
There is a unit test for sqlite-net-pcl which seems to indicate it's working.

Related

The LINQ expression could not be translated - EF Core

In summary, I'm guessing I can't add any more complex calculations to the LINQ expression. Any tips are greatly appreciated!
This blazor project is using a messy employee table which contains two types of employees, both on the same table
Domestic employees, uses NRG number to identify them, but their NRG numbers are stored as string at NRG column, like "0356".
Foreign employees, also uses NRG to identify them, but their NRG column contains all NULL, their NRG numbers are inside their emails at AzureEmail column, like "johndoe.0356#aaa-bbb.com"
When domestic employee or foreign employee enter their sales records, they are the "Closer", it is required to enter the "Setter" NRG.
By using the "Setter" NRG number "closer" entered, I want to locate the "Setter" info from the same employee table:
public async Task Save_to_SalesForm()
{
await using var context3 = await DBContextFactory.CreateDbContextAsync();
{
if (salesForm.SetterNrg != null && salesForm.CsTransferCategory == "Local Team")
{
setterEmployee = context3.Employees.Where(
e => e.AzureAccountEnabled == 1
&&
(int?)(object?)e.Nrg == salesForm.SetterNrg
).OrderByDescending(e => e.EmployeeId).FirstOrDefault();
salesForm.SetterAgentFullName = setterEmployee.AzureFullName;
salesForm.SetterJobTitle = setterEmployee.AzureRole;
salesForm.SetterEmail = setterEmployee.AzureEmail;
salesForm.SetterTeam = setterEmployee.AzureTeam;
}
if (salesForm.SetterNrg != null && salesForm.CsTransferCategory == "CSR Team (Philippines)")
{
setterEmployee = context3.Employees.Where(
e => e.Nrg == null
&&
e.AzureAccountEnabled == 1
&&
e.AzureEmail.Contains("#aaa-bbb.com")
&&
(int?)(object?)e.AzureEmail.Split(new char[] { '.', '#' }, StringSplitOptions.RemoveEmptyEntries)[1] == salesForm.SetterNrg
).OrderByDescending(e => e.EmployeeId).FirstOrDefault();
salesForm.SetterAgentFullName = setterEmployee.AzureFullName;
salesForm.SetterJobTitle = setterEmployee.AzureRole;
salesForm.SetterEmail = setterEmployee.AzureEmail;
salesForm.SetterTeam = setterEmployee.AzureTeam;
}
}
context3.SalesForms.Add(salesForm);
await context3.SaveChangesAsync();
}
If the "Setter" is a domestic employee (Local Team), the above query works fine and be able to save the setter info to the table
If the "Setter" is a foreign employee (CSR Team (Philippines)), the above query won't work due to the .Split make the query too complex for LINQ expression. Error screenshot
I tried multiple ways to resolve the issue, but none seemed ideal.
I have rewritten your query to use EndsWith, which is translatable to the SQL:
public async Task Save_to_SalesForm()
{
await using var context3 = await DBContextFactory.CreateDbContextAsync();
if (salesForm.SetterNrg != null)
{
Employee? setterEmployee = null;
if (salesForm.CsTransferCategory == "Local Team")
{
setterEmployee = await context3.Employees
.Where(e => e.AzureAccountEnabled == 1
&& (int?)(object?)e.Nrg == salesForm.SetterNrg)
.OrderByDescending(e => e.EmployeeId)
.FirstOrDefaultAsync();
}
else if (salesForm.CsTransferCategory == "CSR Team (Philippines)")
{
var toCheck = $".{salesForm.SetterNrg}#aaa-bbb.com";
setterEmployee = await context3.Employees
.Where(e => e.Nrg == null && e.AzureAccountEnabled == 1
&& e.AzureEmail.EndsWith(toCheck))
.OrderByDescending(e => e.EmployeeId)
.FirstOrDefaultAsync();
}
if (setterEmployee != null)
{
salesForm.SetterAgentFullName = setterEmployee.AzureFullName;
salesForm.SetterJobTitle = setterEmployee.AzureRole;
salesForm.SetterEmail = setterEmployee.AzureEmail;
salesForm.SetterTeam = setterEmployee.AzureTeam;
}
}
context3.SalesForms.Add(salesForm);
await context3.SaveChangesAsync();
}
The problem is in e.AzureEmail.Contains("#aaa-bbb.com"), there is no equivalent in sql to this. Try EF.Functions.Like(e.AzureEmail, "%#aaa-bbb.com%"). Everything from your expression will work if you materialize your data with .ToList() or something and perform it on the client, but it is extremely inefficient.

MongoDB projection toListAsync() method not supported

I am trying to use projection in my query and get the following error:
"The result operation MongoDB.Driver.Linq.Expressions.ResultOperators.ListResultOperator is not supported."
Here is the code:
public async Task<IEnumerable<Listing>> LoadAllUserListings(string userId)
{
var result = _context.Listing.Aggregate().Match(l => l.OwnerId == userId || l.Sales.Any(a => a.Owner.Id == userId)).
Project(l => new Listing
{
Id = l.Id,
Reference = l.Reference,
OwnerId = l.OwnerId,
Sales = l.Sales.Where(a => a.Owner.Id == userId || a.Manager.Id == userId).ToList(),
Products = l.Products,
Status = l.Status,
DueDate = l.DueDate
}).ToListAsync();
return await result;
}
It does not appear to like the ToListAsync call. I got this code snippet from the following answer:
https://stackoverflow.com/questions/50904811/mongodb-c-sharp-filter-and-get-all-subdocuments
There reason I am using projection is to omit some fields which the user should not see (depending on the role). Any help on this would be appreciated.
Thanks in advance.
The problem occurs in that line:
Sales = l.Sales.Where(a => a.Owner.Id == userId || a.Manager.Id == userId).ToList()
What happens here ? MongoDB driver takes this expression and tries to translate it to aggregation framework syntax. There's a $filter operator which can be run on nested collection and driver is able to translate .Where() to that operator however there's nothing corresponding for .ToList() at the end of that expression and that's why it fails.
So the fix is fairly easy: you just need to use IEnumerable<T> instead of List<T> for Sales property and then get rid of that .ToList() so your code will look like this:
public async Task<IEnumerable<Listing>> LoadAllUserListings(string userId)
{
var result = _context.Listing.Aggregate().Match(l => l.OwnerId == userId || l.Sales.Any(a => a.Owner.Id == userId)).
Project(l => new Listing
{
Id = l.Id,
Reference = l.Reference,
OwnerId = l.OwnerId,
Sales = l.Sales.Where(a => a.Owner.Id == userId || a.Manager.Id == userId),
Products = l.Products,
Status = l.Status,
DueDate = l.DueDate
}).ToListAsync();
return await result;
}

Non-mandatory fields in the search

I have this piece of the code intended to search a database. A user should have 3 options here: to type surname only, the first name and the user can search using both of them - surname and the first name.
This code retrieves the records from my db if I provide both strings - surname and the first name. But if I type only one of them, my resulting list is always empty.
var query = from x in db.people
where (txtSurname == null || x.Surname== txtSurname.Text)
&& (txtFirstName == null || x.FirstName == txtFirstName.Text)
select x;
var data = query.ToList();
peopleBindingSource.DataSource = data;
Remember that an Entity Framework query doesn't get sent to the database until you materialise the data wth ToList or iterating over it for example. This means you can build up the query in code like this:
var query = db.people.AsQueryable();
if(!string.IsNullOrEmpty(txtSurname.Text))
{
query = query.Where(p => p.Surname == txtSurname.Text);
}
if(!string.IsNullOrEmpty(txtFirstName.Text))
{
query = query.Where(p => p.FirstName == txtFirstName.Text);
}
peopleBindingSource.DataSource = query.ToList();

C# Mongo Driver : "$project or $group does not support Sum([Cost])"

I'm trying to get a result using this query with Linq. I use Mongo Driver version 2.1.1 and I receive this error: "$project or $group does not support Sum([Cost])":
lstSavings = (from data in collection.AsQueryable<SavingsDTOMongo>()
where data.ProjectId.Equals(projectId) &
//((projectConsumptionId != default(Guid)) ? data.ProjectConsumptionId == projectConsumptionId : true) &
plantIds.Contains(data.PlantId) &
data.DateTime >= startDate & data.DateTime <= endDate
group data by new { projectId = data.ProjectId, plantId = data.PlantId, cost = data.Cost } into groupedData
select new SavingsDTO
{
ProjectId = groupedData.Key.projectId,
PlantId = groupedData.Key.plantId,
BaselineConsumption = groupedData.Sum(x => x.BaselineConsumption),
BaselineCost = groupedData.Sum(x => x.BaselineCost),
Consumption = groupedData.Sum(x => x.Consumption),
Cost = groupedData.Sum (x => x.Cost)
ConsumptionSavings = groupedData.Sum(x => x.ConsumptionSavings),
CostSavings = groupedData.Sum(x => x.CostSavings)
}).ToList();
The driver doesn't allow me to select the total sum of different fields, as BaselineCost, consumption, ....
It only happens if I have the condition "plantIds.Contains(data.PlantId). If I delete this condition, it works.
I found a solution. Group data with Lambda Expression. But I would like to group data with LinQ because the aggregation operation is executed directly in mongoServer.
Somebody knows how to solve this driver bug to allows select with contains condition and sum(results) from grouped data?

Unwieldy LINQ statement with multiple search criteria

I have a form with multiple search criteria that a user can use to search for employee data, e.g. FirstName, LastName, HireDate, Department, etc.
I am using LINQ and am wondering what method could I use to query a collection of Employes given any of of the search criteria, i.e. a user does not have to enter all, but they do have to enter at least one of the search parameters.
So far, while testing my LINQ statement with two search parameters in place, it seems that I have to see if the search parameter is entered or not.
If this is the case, then this can get quite unwieldy for many search parameters.
// only FirstName is entered
if (!string.IsNullOrEmpty(FirstName) && string.IsNullOrEmpty(LastName))
{
var employees = DB.Employees
.Where(emp => emp.FirstName.Contains(fName));
}
// only LastName is entered
else if (string.IsNullOrEmpty(FirstName) && !string.IsNullOrEmpty(LastName))
{
var employees = DB.Employees
.Where(emp => emp.LastName.Contains(lName));
}
// both parameters are entered
else if (!string.IsNullOrEmpty(FirstName) && !string.IsNullOrEmpty(LastName))
{
var employees = DB.Employees
.Where(emp => emp.FirstName.Contains(fName))
.Where(emp => emp.LastName.Contains(lName));
}
FYI, I initially thought that I could just append Where() statements to my LINQ statement with the pertinent search parameters but I noticed that not all records were being returned that should and thus the above logic of if-then statements.
What about something like this:
IQueryable<Employee> employees = DB.Employees;
if (!string.IsNullOrEmpty(FirstName))
{
employees = employees
.Where(emp => emp.FirstName.Contains(fName));
}
if (!string.IsNullOrEmpty(LastName))
{
employees = employees
.Where(emp => emp.Last.Contains(lName));
}
You can write it like this:
var employees = DB.Employees.AsQueryable();
if (!string.IsNullOrEmpty(fName)
employees = employees.Where(emp => emp.FirstName.Contains(fName));
if (!string.IsNullOrEmpty(lName)
employees = employees.Where(emp => emp.LastName.Contains(lName));
I encountered a similar challenge where a user could select 0, 1 or many values for about 10 searchable fields and needed to construct that query at runtime.
I ended up using LINQKit:
http://www.albahari.com/nutshell/linqkit.aspx
In particular I used it's predicate builder, which is described here:
http://www.albahari.com/nutshell/predicatebuilder.aspx
In your example above, you've encompassed the query multiple within if statements.
The alternative is to build the query as you go.
If you were to declare var employees = DB.Employees outside of those if statements (Assuming that it's always relevant), then you could just tack on your where statements within your if statements if they're applicable.
LINQ gives you deferred execution, so you don't have to have the entire expression in a single block (Even though it feels most natural to do so and in many cases you will).
Things get a bit more complicated if you want to mix in OR's with ANDs, but that's where the previously mentioned predicate builder comes in.
Unfortunately I don't have any examples to share, but those links should get you off to a good start.
var resultData = (from data in db.Abc
where !string.IsNullOrEmpty(firstName) ? data.FirstName == firstName : true
&& data.UserType == userTypeValue
&& !string.IsNullOrEmpty(lastName) ? data.LastName == lastName : true
&& !string.IsNullOrEmpty(gender) ? data.Gender == gender : true
&& !string.IsNullOrEmpty(phone) ? data.CellPhone == phone : true
&& !string.IsNullOrEmpty(fax) ? data.Fax == fax : true
&& !string.IsNullOrEmpty(emailAddress) ? data.Email == emailAddress : true
&& !string.IsNullOrEmpty(address1) ? data.Address == address1 : true
select new
{
UserName = data.UserName,
FirstName = data.FirstName,
Address = data.Address,
CellPhone = data.CellPhone,
Fax = data.Fax,
Email = data.Email
}).ToList();

Categories