Cakephp3: Result of CASE statement being forced to string, need integer - cakephp

I am generating a case statement to return either 0 or 1:
$desc_case = $q->newExpr()
->addCase(
[$q->newExpr()->add(["description IS" => NULL])],
[0,1],
["integer","integer"]
);
$q = $q->select(["has_desc" => $desc_case]);
Which results in the following correct SQL:
SELECT [fields removed for clarity], (CASE WHEN (description) IS NULL THEN :c0 ELSE :c1 END) AS `has_desc` FROM skills Skills
I've turned hydration off, and retrieved the result with
->hydrate(false)->toArray();
The result of the CASE statement is returned as a string- either "0" or "1" - which is messing up logic downstream.
I've traced the execution code as best I can, and it looks like CakePHP is using the type names passed to correctly bind the values, but nowhere does the type make its way into the TypeMap used for mapping output.
An easy workaround is to adjust the values after the fact (which I'm doing), but I'd like to make this work as expected on principle... :)

The type information passed to addCase() is only ment to be used for input casting, ie the values passed will be bound as the given types. The return values, ie the values being selected via the compiled CASE statement, are in no way being affected.
If you want to affect the type used for casting selected values of columns that do not exist in the schema (note that changing the schema will also affect other parts of the ORM and the query builder), then you have to change the type map accordingly, for example:
$query
->getSelectTypeMap()
->addDefaults([
'has_desc' => 'integer'
]);
See also
Cakephp-3.x: How to change the data type of a selected alias?

Related

CakePHP 3: order of translated data with missing translation strings

I use TranslationBehavior to handle translated data in my app. When retrieving data, CakePHP returns the translated string or if this string is not available, the value of the default locale. To request the data, the following query is used:
$items = $this->Model->find('all')
->all()
->extract('name');
To order the output, the query is:
$items = $this->Model->find('all')
->order([$this->Model->translationField('name') => 'ASC'])
->all()
->extract('name');
This works for all default locale items and all translated items. But when the translation for a record is missing, it will break the order. In this case the correct fallback value is returned, but the order is no longer correct. The output looks like this:
['A... (Translated)', 'B... (Translated)', 'A... (Default)', 'C... (Default)']
What I expect is the following order:
['A... (Default)', 'A... (Translated)', 'B... (Translated)', 'C... (Default)']
To achieve this, I changed the query to:
$items = $this->Model->find('all')
->order(['IF('.$this->Model->translationField('name').' != "", '.$this->Model->translationField('name').', Model.name)' => 'ASC'])
->all()
->extract('name');
Which gives the expected order:
['A... (Default)', 'A... (Translated)', 'B... (Translated)', 'C... (Default)']
The question is: is this the correct way to handle the order of mixed locales? Or did I miss something and CakePHP already provides a simpler solution?
You may ask, why should we mix the locales? In my case, it's not necessary to translate all the strings, because some of the items are identical in both languages.
Looking at how CakePHP queries the translated fields, and merges them later on at PHP level, this is the expected behavior, and you'd indeed have to use a conditional expression in the ORDER clause.
I'd suggest to use a CASE expression, as the IF() function is MySQL specific, something like:
$query = $this->Model->find();
$query
->orderAsc(
$query->newExpr()->addCase(
[
$query->newExpr()->isNotNull($this->Model->translationField('name'))
],
[
$query->identifier($this->Model->translationField('name')),
$query->identifier('Model.name')
]
)
)
// ...
Which would generate an expression similar to:
ORDER BY
CASE WHEN Model_name_translation.content IS NOT NULL
THEN Model_name_translation.content
ELSE Model.name END
ASC
And as mentioned in the comments, If you go the "do not translate identical strings" route, then you should avoid storing empty strings for such "missing" translations, instead do not store a record for them at all.
In order to avoid all this you could of course add translations for everything, even if they are equal in the different languages.

Power Query M loop table / lookup via a self-join

First of all I'm new to power query, so I'm taking the first steps. But I need to try to deliver sometime at work so I can gain some breathing time to learn.
I have the following table (example):
Orig_Item Alt_Item
5.7 5.10
79.19 79.60
79.60 79.86
10.10
And I need to create a column that will loop the table and display the final Alt_Item. So the result would be the following:
Orig_Item Alt_Item Final_Item
5.7 5.10 5.10
79.19 79.60 79.86
79.60 79.86 79.86
10.10
Many thanks
Actually, this is far too complicated for a first Power Query experience.
If that's what you've got to do, then so be it, but you should be aware that you are starting with a quite difficult task.
Small detail: I would expect the last Final_Item to be 10.10. According to the example, the Final_Item will be null if Alt_Item is null. If that is not correct, well that would be a nice first step for you to adjust the code below accordingly.
You can create a new blank query, copy and paste this code in the Advanced Editor (replacing the default code) and adjust the Source to your table name.
let
Source = Table.Buffer(Table1),
AddedFinal_Item =
Table.AddColumn(
Source,
"Final_Item",
each if [Alt_Item] = null
then null
else List.Last(
List.Generate(
() => [Final_Item = [Alt_Item], Continue = true],
each [Continue],
each [Final_Item =
Table.First(
Table.SelectRows(
Source,
(x) => x[Orig_Item] = [Final_Item]),
[Alt_Item = "not found"]
)[Alt_Item],
Continue = Final_Item <> "not found"],
each [Final_Item])))
in
AddedFinal_Item
This code uses function List.Generate to perform the looping.
For performance reasons, the table should always be buffered in memory (Table.Buffer), before invoking List.Generate.
List.Generate is one of the most complex Power Query functions.
It requires 4 arguments, each of which is a function in itself.
In this case the first argument starts with () and the other 3 with each (it should be clear from the outline above: they are aligned).
Argument 1 defines the initial values: a record with fields Final_Item and Continue.
Argument 2 is the condition to continue: if an item is found.
Argument 3 is the actual transformation in each iteration: the Source table is searched (with Table.SelectRows) for an Orig_Item equal to Alt_Item. This is wrapped in Table.First, which returns the first record (if any found) and accepts a default value if nothing found, in this case a record with field Alt_Item with value "not found", From this result the value of record field [Alt_Item] is returned, which is either the value of the first record, or "not found" from the default value.
If the value is "not found", then Continue becomes false and the iterations will stop.
Argument 4 is the value that will be returned: Final_Item.
List.Generate returns a list of all values from each iteration. Only the last value is required, so List.Generate is wrapped in List.Last.
Final remark: actual looping is rarely required in Power Query and I think it should be avoided as much as possible. In this case, however, it is a feasible solution as you don't know in advance how many Alt_Items will be encountered.
An alternative for List.Generate is using a resursive function.
Also List.Accumulate is close to looping, but that has a fixed number of iterations.
This can be solved simply with a self-join, the open question is how many layers of indirection you'll be expected to support.
Assuming just one level of indirection, no duplicates on Orig_Item, the solution is:
let
Source = #"Input Table",
SelfJoin1 = Table.NestedJoin( Source, {"Alt_Item"}, Source, {"Orig_Item"}, "_tmp_" ),
Expand1 = ExpandTableColumn( SelfJoin1, "_tmp_", {"Alt_Item"}, {"_lkp_"} ),
ChkJoin1 = Table.AddColumn( Expand1, "Final_Item", each (if [_lkp_] = null then [Alt_Item] else [_lkp_]), type number)
in
ChkJoin1
This is doable with the regular UI, using Merge Queries, then Expand Column and adding a custom column.
If yo want to support more than one level of indirection, turn it into a function to be called X times. For data-driven levels of indirection, you wrap the calls in a list.generate that drop the intermediate tables in a structured column, though that's a much more advanced level of PQ.

In CakePHP 3.X using addCase how should I define a value as a fallback? [duplicate]

I'm trying to get a query working using a case statement, and can't figure out how to get the case to return a column value instead of a constant. I have the query working perfectly, except that the column names I'm providing for the results are being quoted or otherwise mishandled by Cake or maybe PDO somewhere down in a layer that I can't dig my way through. I got as far down as bindValue, but none of the documentation I encountered along the way tells me how to do this.
I have found this example comment:
$statement->bindValue(1, 'a title');
$statement->bindValue(2, 5, PDO::INT);
$statement->bindValue('active', true, 'boolean');
$statement->bindValue(5, new \DateTime(), 'date');
but in all these cases, the value provided is a constant. I need to pass in a string that is the name of the column that I want returned.
I tried both 'string' (resulted in quoted column name) and 'integer' (resulted in 0). I tried PDO::FETCH_COLUMN (seemed highly unlikely, but looked like the next best bet from http://php.net/manual/en/pdo.constants.php, and easy to try it...). I tried 'literal', inspired by the way you can put literal strings into expressions (resulted in Error: unknown type "literal"). That error message led me to src/Database/Type.php, but nothing in there helped me either.
So, I'm pretty much stumped. Here's a simple version of the code I have (leaving out a couple of conditions and unrelated columns):
$query = $this->Games->find();
$team_id = $query->newExpr()->addCase(
[$query->newExpr()->eq('Games.status', 'home_default')],
['home_team_id', 'away_team_id'],
['string', 'string']
);
$defaulting = $query
->select([
'id' => $team_id,
'count' => 'COUNT(Games.id)',
])
->where([
'Games.status IN' => ['home_default', 'away_default'],
])
->group('id')
->toArray();
This generates this SQL:
SELECT
(CASE WHEN Games.status = 'home_default'
THEN 'home_team_id' ELSE 'away_team_id' END) AS `id`,
COUNT(Games.id) AS `count`
FROM games Games
WHERE Games.status in ('home_default','away_default')
GROUP BY id
Note that THEN 'home_team_id' ELSE 'away_team_id' END should be simply THEN home_team_id ELSE away_team_id END. This will then allow me to read the list of ids of teams that have defaulted games along with the number of games they defaulted.
By default the values passed to the second argument of QueryExpression::addCase() are being treated as to be converted to literal values, not as identifiers. If you need the latter, then you should use an expression, an IdentifierExpression.
use Cake\Database\Expression\IdentifierExpression;
// ...
$team_id = $query->newExpr()->addCase(
[
$query->newExpr()->eq('Games.status', 'home_default')
],
[
new IdentifierExpression('Games.home_team_id'),
new IdentifierExpression('Games.away_team_id')
]
);
Also ditch the third argument in this case, you don't want the values to be string literals (for expressions the types would be ignored anyways).

Retrieve ContentPart using IContentManager, filtered by case-insensitive field

In the development of an Orchard module, how do I retrieve ContentParts case insensitively filtered by a field? I have tried
var name = viewModel.Name.ToUpper();
var samples = _contentManager.Query<SamplePart, SamplePartRecord>()
.Where(x => x.Name.ToUpper() == name)
.List();
and I'm getting an error
Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
but when I tried to retrieve without bothering if it's case sensitive
var name = viewModel.Name;
var samples = _contentManager.Query<SamplePart, SamplePartRecord>()
.Where(x => x.Name == name)
.List();
No errors reported.
What gives?
Be aware that the expression inside the Where clause is being translated by NHibernate to an SQL query at some point. Hence you're pretty restricted to what you can do there. In this case it seems like the ToUpper method is not supported.
Another thing - the string comparison behavior in SQL Server depends on the actual collation set on your database. By default it's case insensitive, so any string comparison will ignore case. So, if you stick to defaults you're good with just an ordinary == on two strings, like in your last example.

Mongoid Syntax Questions

1) Finding by instance object
Assuming I have the instance object called #topic. I want to retrieve the answers for this given topic. I was thinking I should be able to pass in :topics=>#topic, but i had to do the very ugly query below.
#answers = Answers.where(:topic_ids => {"$in" => [#topic.id]})
2) Getting the string representation of the id. I have a custom function (shown below). But shouldn't this be a very common requirement?
def sid
return id.to_s
end
If your associations are set up correctly, you should be able to do:
#topic.answers
It sounds like the above is what you are looking for. Make sure you have set up your associations correctly. Mongoid is very forgiving when defining associations, so it can seem that they are set up right when there is in fact a problem like mismatched names in references_many and referenced_in.
If there's a good reason why the above doesn't work and you have to use a query, you can use this simple query:
#answers = Answer.where(:topic_ids => #topic.id)
This will match any Answer record whose topic_ids include the supplied ID. The syntax is the same for array fields as for single-value fields like Answer.where(:title => 'Foo'). MongoDB will interpret the query differently depending on whether the field is an array (check if supplied value is in the array) or a single value (check if the supplied value is a match).
Here's a little more info on how MongoDB handles array queries:
http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-ValueinanArray

Resources