How to determine if an IQueryable expression needs additional processing - sql

By additional processing, I mean besides the standard LINQ to SQL translation to Transact SQL (and possible work arounds)
I’ve two issues that I was hoping for some insight on and/or some appropriate links or Google terms to use to find more information on as I’m not finding anything. It boils down to the fact that I would like to find out when/how an IQueryable expression that is going to be executed determines that some of the expression result needs to be run ‘client side’, meaning that the LINQ expression cannot be translated directly to Transact SQL to return the entire result.
This is in regards to my post/code.
Situation 1
string.Format() does not translate to Transact SQL. I can accept that, but guess I was just looking for some advice on how to detect if an Expression will be able to fully translate to Transact SQL.
LINQ Expression:
from h in HistoryData
select new { NewData = string.Format( "New Data: {0}", h.hisData ) };
Provider Context SQL:
SELECT [t0].[hisData] AS [arg0]
FROM [HistoryData] AS [t0]
My Workaround: Currently, I’m using DataContext.GetCommand() method then looking at the Transact SQL and searching for [arg0]. Obviously not ideal, so I was looking for a more robust mechanism (possibly spotting something with an ExpressionTree visitor??)
Situation 2
Using the tertiary operator in some situations seems to return some Transact SQL that obviously has some post/client processing applied to it to get the proper values. In the examples below, I’m running within the context of LINQPad (thus the Dump() extension method is available). There seems to be two situations when post processing is needed when a tertiary operator is in play:
When a boolean variable along with a boolean field is used (var testBool… expression below) and
When two variables, regardless of type, are used and no database fields are queried (the second testNumber expression below).
This situation surprised me that client side processing was needed. So in addition to learning how to properly detect when an expression needs additionally processing outside of the normal Transact SQL, if anyone has any insight as to why L2S couldn’t execute a simple CASE statement like the other situations and more importantly a possible workaround that could be used to avoid any ‘client side’ processing that would be great!
LINQ Expressions:
var before9_1_2009 = DateTime.Today < new DateTime( 2009, 9, 1 );
var fiveThousand = 5000;
var tenThousand = 10000;
var testNumber =
Profiles.Where( p => p.pAuthID == "111111111" )
.Select( p => new { Number = before9_1_2009 ? fiveThousand : p.pKey } );
var cmd = GetCommand( testNumber );
cmd.CommandText.Dump( "Number Property: Works (i.e. evaluates on SQL Server)" );
var testString =
Profiles.Where( p => p.pAuthID == "111111111" )
.Select( p => new { String = before9_1_2009 ? "StringConstant" : p.pAuthID } );
cmd = GetCommand( testString );
cmd.CommandText.Dump( "String Property: Works (i.e. evaluates on SQL Server)" );
var testBool =
Profiles.Where( p => p.pAuthID == "111111111" )
.Select( p => new { Boolean = boolExp ? true : p.pProcessed } );
cmd = GetCommand( testBool );
cmd.CommandText.Dump( "Boolean Property: Has NULL AS [EMPTY] - Post Processing Needed" );
testNumber =
Profiles.Where( p => p.pAuthID == "111111111" )
.Select( p => new { Number = before9_1_2009 ? fiveThousand : tenThousand } );
cmd = GetCommand( testNumber );
cmd.CommandText.Dump( "Number Property (using two constants): Has NULL AS [EMPTY] - Post Processing Needed" );
Provider Context SQL (in order):
▪ Number Property: Works (i.e. evaluates on SQL Server)
SELECT
(CASE
WHEN #p1 = 1 THEN #p2
ELSE [t0].[pKey]
END) AS [Number]
FROM [Profile] AS [t0]
WHERE [t0].[pAuthID] = #p0
▪ String Property: Works (i.e. evaluates on SQL Server)
SELECT
(CASE
WHEN #p1 = 1 THEN CONVERT(NVarChar(255),#p2)
ELSE [t0].[pAuthID]
END) AS [String]
FROM [Profile] AS [t0]
WHERE [t0].[pAuthID] = #p0
▪ Boolean Property: Has NULL AS [EMPTY]
SELECT NULL AS [EMPTY]
FROM [Profile] AS [t0]
WHERE [t0].[pAuthID] = #p0
▪ Number Property (using two constants): Has NULL AS [EMPTY]
SELECT NULL AS [EMPTY]
FROM [Profile] AS [t0]
WHERE [t0].[pAuthID] = #p0
My Workaround: Again, I’m using DataContext.GetCommand() method then looking at the Transact SQL and searching for NULL AS [EMPTY]. Wondering if there are any other ‘magic strings’ that are going to appear for expression code that cannot run on the server…so as above, I’m looking for a correct, more robust way of detecting these situations.

Quick observations:
Where did you declare boolExp variable?
Number Property (using two constants): 2 var objects return ambiguous type.
(for first Number case p.pKey defined type)

Related

How can use executeQueryWithParameters with SQLBuilderSelectExpression to join an x++/sql statement in Microsoft Dynamics?

In Dynamics 365 for Finance and Operations, they describe a method of creating SQL statements "as objects, as opposed to text", but this is somewhat of a lie. They use the objects to create the text which then populates str sqlStatement = selectExpr.getExpression(null);
This sqlStatement would then feed the obsolete statement.executeQuery(sqlStatement);.
I can make the warning go away by using executeQueryWithParameters() with an empty map (SqlParams::create()) as the second parameter, but this seems to be "cheating".
Is there a way I can/should refactor the following to populate the map correctly?
SQLBuilderSelectExpression selectExpression = SQLBuilderSelectExpression::construct();
selectExpression.parmUseJoin(true);
SQLBuilderTableEntry vendTable = selectExpression.addTableId(tableNum(VendTable));
SQLBuilderTableEntry dirPartyTable = vendTable.addJoinTableId(tableNum(DirPartyTable));
SQLBuilderFieldEntry accountNum = vendTable.addFieldId(fieldNum(VendTable, AccountNum));
SQLBuilderFieldEntry name = dirPartyTable.addFieldId(fieldNum(DirPartyTable, Name));
SQLBuilderFieldEntry dataAreaId = vendTable.addFieldId(fieldNum(VendTable, dataAreaId));
SQLBuilderFieldEntry blocked = vendTable.addFieldId(fieldNum(VendTable, Blocked));
vendTable.addRange(dataAreaId, curext());
vendTable.addRange(blocked, CustVendorBlocked::No);
selectExpression.addSelectFieldEntry(SQLBuilderSelectFieldEntry::newExpression(accountNum, 'AccountNum'));
selectExpression.addSelectFieldEntry(SQLBuilderSelectFieldEntry::newExpression(name, 'Name'));
str sqlStatement = selectExpression.getExpression(null);
// FIXME:
ResultSet resultSet = statement.executeQueryWithParameters(sqlStatement, SqlParams::create());
Below is how you would write your code as a standard X++ query. However, I must note that what you're doing may not be the best approach.
DirPartyTable is a special table in AX as it supports inheritance, so you should make sure you fully understand the framework. See:
https://learn.microsoft.com/en-us/dynamicsax-2012/appuser-itpro/implementing-the-global-address-book-framework-white-paper
https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/fin-ops/organization-administration/overview-global-address-book
Code:
VendTable vendTable;
DirPartyTable dirPartyTable;
while select AccountNum from vendTable
where vendTable.Blocked == CustVendorBlocked::No
// DataAreaId along with Partition, are automatically included in the query context depending
// on the company context you're executing the code from
// && vendTable.dataAreaId == curext()
join Name from dirPartyTable
where dirPartyTable.RecId == vendTable.Party
{
info(strFmt("Account: %1; Name: %2", vendTable.AccountNum, dirPartyTable.Name));
}
Regarding an AOT query, look in the AOT at \Queries\VendTableListPage and expand the data sources and learn from it.
Regardless of what OP is trying to do with the query, the answer to the question of "how do I correctly replace executeQuery with executeQueryWithParameters" can be found in the following article.
https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/dev-ref/query-with-parameters
The new *WithParameters APIs were introduced as a way to mitigate sql injection attacks which may occur when building up sql strings manually with un-sanitized sql parameters as input.
Snippet of the code example from above doc shows how to correctly populate the map to match the sql statement:
str sql = #"
UPDATE Wages
SET Wages.Wage = Wages.Wage * #percent
WHERE Wages.Level = #Level";
Map paramMap = SqlParams::create();
paramMap.add('percent', 1.1); // 10 percent increase
paramMap.add('Level', 'Manager'); // Management increase
int cnt = statement.executeUpdateWithParameters(sql, paramMap);

Linq Query in VB

Good Day,
I am querying my database using Linq and I have run into a problem, the query searched a column for a search phrase and based on if the column has the phrase, it then returns the results, The query is below,
Dim pdb = New ProductDataContext()
Dim query =
From a In pdb.tblUSSeries
Join b In pdb.tblSizes_ On a.Series Equals b.Series
Where
a.Series.ToString().Equals(searchString) Or
b.Description.Contains(searchString) Or Not b.Description.Contains(Nothing)
Order By b.Series, b.OrderCode Ascending
Select New CustomSearch With
{
.Series = a.Series,
.SeriesDescription= a.Description,
.Coolant = a.Coolant,
.Material = a.Material,
.Standard = a.Standard,
.Surface = a.Surface,
.Type = a.Type,
.PointAngle = a.PointAngle,
.DiaRange = a.DiaRange,
.Shank = b.Shank,
.Flutes = b.Flutes,
.EDPNum = b.EDPNum,
.SizesDescription = b.Description,
.OrderCode = b.OrderCode
}
Return query
I think the problem is that, in the table certain rows are NULL, so when it is checking the column for the phrase and it encounters a row that is null it, breaks and returns this error,
The cast to value type 'System.Int32' failed because the materialized value is null. Either the result type's generic parameter or the query must use a nullable type.
I have ran this query against another column that has all the rows populated with data and it returns the results ok.
So my question is how can I write it in VB to query the db with the supplied searchstring and return the results, when some of the rows in the columns have null values.
Any help would be great.
The exception occurs when you make the projection (i.e. select new CustomSearch)
And yes your trying to assign Null to some int property
(Not sure which one of your properties that is)
one of 2 choices :
1) Use nullalbe types for your properties (or just that one property).
2) project with an inline If ( ?? in C#) , I don't know VB so don't catch me on the syntax.
Taking Series just as an example i don't know if it's an int or if that's the problematic property
Select New CustomSearch With
{
.Series = If(a.Series Is Nothing,0, CInt(a.Series))
}
In C#
Select new CustomSearch
{
Series = a.Series ?? 0;
}

SQL 'comment' that can be read in a sql profiler

I've tried several methods such as using double hyphens, i.e. --THIS IS A COMMENT but when the executed sql is read in a profiler the comment is stripped out leaving only raw SQL that is being performed.
I want to do this to enable rapid identification of queries and their origins when looking at a SQL Profilers output that has over 8000 entries per minute,
so something like
--Method signature and an application name
e.g.
--MyMethod(string username) in MyFunkyAppName.
I'm using EntityFramework 4.3 which complicates things even further with linq to entities and a smattering of linq to sql thrown in for good measure.
EDIT: I'm aware of solutions to add a dodgy where clause or use anonymous properties to identify things such as Clever tricks to find specific LINQ queries in SQL Profiler but I'm hoping for a far less hacky approach or perhaps a generic one.
Here is an extension method you can use to tag your Entity Framework queries. It uses the WHERE clause, but shouldn't impair performance.
public static class ExtensionMethods
{
public static IQueryable<T> SetQueryName<T>(this IQueryable<T> source,
[CallerMemberName] String name = null,
[CallerFilePath] String sourceFilePath = "",
[CallerLineNumber] Int32 sourceLineNumber = 0)
{
var expr = Expression.NotEqual(Expression.Constant("Query name: " + name), Expression.Constant(null));
var param = Expression.Parameter(typeof(T), "param");
var criteria1 = Expression.Lambda<Func<T, Boolean>>(expr, param);
expr = Expression.NotEqual(Expression.Constant($"Source: {sourceFilePath} ({sourceLineNumber})"), Expression.Constant(null));
var criteria2 = Expression.Lambda<Func<T, Boolean>>(expr, param);
return source.Where(criteria1).Where(criteria2);
}
}
Here is how to use it:
context.Table1.SetQueryName().Where(x => x.C1 > 4)
It will use the calling method name as the query name.
You can specify another name like this:
context.Table1.SetQueryName("Search for numbers > 4").Where(x => x.Number > 4)
Here is how the SQL will look like:
SELECT
[Extent1].[Number] AS [Number]
FROM (SELECT
[Table1].[Number] AS [Number]
FROM [dbo].[Table1] AS [Table1]) AS [Extent1]
WHERE
(N'Query name: Search for numbers > 4' IS NOT NULL)
AND
(N'Source: C:\Code\Projects\MyApp\Program.cs (49)' IS NOT NULL)
AND ([Extent1].[Number] > 4)

Nhibernate query condition within sum

I am trying the following code but nhibernate is throwing the following exception:
Expression type 'NhSumExpression' is not supported by this SelectClauseVisitor.
var data =
(
from a in session.Query<Activity>()
where a.Date.Date >= dateFrom.Date && a.Date.Date <= dateTo.Date
group a by new { Date = a.Date.Date, UserId = a.RegisteredUser.ExternalId } into grp
select new ActivityData()
{
UserID = grp.Key.UserId,
Date = grp.Key.Date,
Bet = grp.Sum(a => a.Amount < 0 ? (a.Amount * -1) : 0),
Won = grp.Sum(a => a.Amount > 0 ? (a.Amount) : 0)
}
).ToArray();
I've been looking around and found this answer
But I am not sure what I should use in place of the Projections.Constant being used in that example, and how I should create a group by clause consisting of multiple fields.
It looks like your grouping over multiple columns is correct.
This issue reported in the NHibernate bug tracker is similar: NH-2865 - "Expression type 'NhSumExpression' is not supported by this SelectClauseVisitor."
Problem is that apart from the less-than-helpful error message, it's not really a bug as such. What happens in NH-2865 is that the Sum expression contains something which NHibernate doesn't know how to convert into SQL, which result in this exception being thrown by a later part of the query processing.
So the question is, what does you sum expression contains that NHibernate cannot convert? The thing that jumps to mind is the use of the ternary operator. I believe the NHibernate LINQ provider has support for the ternary operator, but maybe there is something in this particular combination that is problematic.
However, I think your expressions can be written like this instead:
Bet = grp.Sum(a => Math.Min(a.Amount, 0) * -1), // Or Math.Abs() instead of multiplication.
Won = grp.Sum(a => Math.Max(a.Amount, 0))
If that doesn't work, try to use a real simple expression instead, like the following. If that works, we at least know the grouping itself work as expected.
Won = grp.Sum(a => a.Amount)

Optional parameter with setParameterList in HQL

I have a query with optional parameter
"SELECT ClinicId,Name from Clinic where :ClinicIds is NULL OR ClinicId IN :ClinicIds"
List<int> ClinicIds = null;
I'm passing the parameter as following
q.SetParameterList("ClinicIds", ClinicIds);
Because ClinicId is an optional parameter. If I pass null to SetParameterList I'm getting exception. Any idea how I can pass an optional parameter(null value) to SetParameterList.
Thanks
Rather than using HQL, use a Criteria query. Its designed to be more programmatic than HQL, in that you use straight Java code to assemble your query, which enables you to use if-then logic. So, instead of either concatenating HQL, or having two different HQL queries which you need to independently maintain, you have one Criteria query which accounts for both situations. Example:
//SELECT ClinicId,Name from Clinic where :ClinicIds is NULL OR ClinicId IN :ClinicIds
Criteria criteria = getSession().createCriteria(Clinic.class);
if(ClinicIds == null) {
criteria.add(Restrictions.eq("ClinicId", null));
} else {
criteria.add(Restrictions.or(
Restrictions.eq("ClinicId", null),
criteria.add(Restrictions.in("ClinicId", ClinicIds));
)
);
}
return criteria.list();
You can't. That would generate invalid SQL.
You need to change the HQL depending on whether there are ClinicIds.
I did it like:
"SELECT ClinicId,Name from Clinic where -1 in (:ClinicIds) OR ClinicId IN :ClinicIds"
and
if(ClinicIds == null or ClinicIds.isEmpty()){
ClinicIds = new List<int>();
ClinicIds.add(-1);
}
q.SetParameterList("ClinicIds", ClinicIds);
just a trick