Datatables server side processing and column alias - datatables

In Datatables using server side processing, is it possible to use column alias when specifying the columns?
At the moment this works fine with:
$aColumns = array( 'datetime','username', 'user_ip', 'company', 'action' );
but I would like to change the format of the date using date format in MySQL so, in effect, I want to use:
$aColumns = array( 'DATE_FORMAT(datetime, "%d/%m/%Y - %H:%i:%s") as newdate';'username'; 'user_ip';'company'; 'action' );
The problem is that the alias has a comma and the aColumns array is comma seperated so it breaks when later, for example, it comes to:
$sQuery = "
SELECT SQL_CALC_FOUND_ROWS ".str_replace(" , ", " ", implode(", ", $aColumns))."
FROM $sTable
$sWhere
$sOrder
$sLimit
";
Is there a way I can use the alias rather than the original value? Even simply changing the select statement does not work as aColumns is used throughout the script hence it needs that value to be set correctly
Thanks

Yes. I actually just struggled with this issue myself. Because the JSON output is determined through counting the amount of columns in the array, and because of the imploding array, you have to add your column alias to $sQuery instead of the $aColumns array. So you'll actually have one less column in your $aColumns array than you'll need. For example, in mine, I needed an alias called total created from multiplying price and qty. So I put all my unaliased columns in the $aColumns array, like this:
$aColumns = array( 'purchaseID', 'dateOfOrder', 'productID', 'price', 'QTY');
But then, in the $sQuery string that concatenates all the things necessary to create the proper query string, I added my column alias between the implode and FROM. Don't forget to put a comma after the implode though, because it doesn't add it for you. The original $sQuery string looks like this:
$sQuery = "
SELECT SQL_CALC_FOUND_ROWS ".str_replace(" , ", " ", implode(", ", $aColumns))."
FROM `$sTable`
$sWhere
$dateSql
$sOrder
$sLimit
";
But mine, with the column alias added, looks like this:
$sQuery = "
SELECT SQL_CALC_FOUND_ROWS ".str_replace(" , ", " ", implode(", ", $aColumns))."
, `price` * `QTY` AS `total` FROM `$sTable`
$sWhere
$dateSql
$sOrder
$sLimit
";
Finally, the last thing you have to do is alter the actual JSON output to make sure your extra column is accounted for in the FOR loop at the end right before the json_encode, because it inserts items into the $row array, which is what becomes 'aaData' (the returned row data), based on how many columns you've specified in the $aColumns array, and because you left out any you've aliased, the count will be wrong, and you will get an error that looks something like 'requested unknown parameter from data source row'. The original FOR loop looks like this:
while ( $aRow = mysql_fetch_array( $rResult ) )
{
$row = array();
for ( $i=0 ; $i<count($aColumns) ; $i++ )
{
if ( $aColumns[$i] == "version" )
{
/* Special output formatting for 'version' column */
$row[] = ($aRow[ $aColumns[$i] ]=="0") ? '-' : $aRow[ $aColumns[$i] ];
}
else if ( $aColumns[$i] != ' ' )
{
/* General output */
$row[] = $aRow[ $aColumns[$i] ];
}
}
$output['aaData'][] = $row;
}
Like I said, this FOR loop works based off the COUNT of the $aColumns array, and since I've added an alias, it's going to cut my results short. It's not going to return the last element in the array containing the returned columns, so I'm going to alter the code to look like this:
for ( $i=0 ; $i<count($aColumns) + 1; $i++ )
{
if ($i < count($aColumns)){
if ( $aColumns[$i] == "version" )
{
/* Special output formatting for 'version' column */
$row[] = ($aRow[ $aColumns[$i] ]=="0") ? '-' : $aRow[ $aColumns[$i] ];
}
else if ( $aColumns[$i] != ' ' )
{
/* General output */
$row[] = $aRow[ $aColumns[$i] ];
}
}
else {
$row[] = $aRow['total'];
}
}
$output['aaData'][] = $row;
}
All I changed was the counter condition from $i<count($aColumns) to $i<count($aColumns) + 1, because my alias makes the column count one higher than what's in the array. And I've added a wrapping if-else that just says that if the counter, $i, is higher than the number of columns I've specified in the $aColumns array, then we've added all the columns in the array to the output data, so because I only added one extra alias column, then that means I can go ahead and just add that to the $row array, which contains all the output data from the returned rows.
You can actually add in as many aliased columns as you need, you just need to scale the code accordingly. Hope this helps!

Related

Can Laravel automatically switch between column = ? and column IS NULL depending on value?

When building a complex SQL query for Laravel, using ? as placeholders for parameters is great. However when the value is null, the SQL syntax needs to be changed from = ? to IS NULL. Plus, since the number of parameters is one less, I need to pass a different array.
To get it to work, I have written it like this, but there must be a better way:
if ($cohortId === null) {
// sql should be: column IS NULL
$sqlCohortString = "IS NULL";
$params = [
Carbon::today()->subDays(90),
// no cohort id here
];
} else {
// sql should be: column = ?
$sqlCohortString = "= ?";
$params = [
Carbon::today()->subDays(90),
$cohortId
];
}
$query = "SELECT items.`name`,
snapshots.`value`,
snapshots.`taken_at`,
FROM snapshots
INNER JOIN (
SELECT MAX(id) AS id, item_id
FROM snapshots
WHERE `taken_at` > ?
AND snapshots.`cohort_id` $sqlCohortString
GROUP BY item_id
) latest
ON latest.`id` = snapshots.`id`
INNER JOIN items
ON items.`id` = snapshots.`item_id`
ORDER by media_items.`slug` ASC
";
$chartData = DB::select($query, $params);
My question is: does Laravel have a way to detect null values and replace ? more intelligently?
PS: The SQL is for a chart, so I need the single highest snapshot value for each item.
You can use ->when to create a conditional where clause:
$data = DB::table('table')
->when($cohortId === null, function ($query) {
return $query->whereNull('cohort_id');
}, function ($query) use ($cohortId) {
// the "use" keyword provides access to "outer" variables
return $query->where('cohort_id', '=', $cohortId);
})
->where('taken_at', '>', $someDate)
->toSql();

How to create constraints that use expr() dynamically?

A user can enter a letter range like "A-D", by which a query must find all records that start with any of those letters. What I eventually need is a constraints block that looks like this:
$constraints = [
$query->expr()->eq(
'composition.sys_language_uid',
$query->createNamedParameter($language, \PDO::PARAM_INT)
),
$query->expr()->orX(
$query->expr()->like(
'composition.title',
$query->createNamedParameter('A%')
),
$query->expr()->like(
'composition.title',
$query->createNamedParameter('B%')
),
$query->expr()->like(
'composition.title',
$query->createNamedParameter('C%')
),
$query->expr()->like(
'composition.title',
$query->createNamedParameter('D%')
)
)
];
which is a structure, that works well, when I use it as a test. So I know I need to strive for a solution like this.
But, of course, since the letters given are not fixed, but variable, the block within ->orX() needs to be calculated programmatically. This is, where my problem lies.
I tried this:
// A custom helper function that splits a letter range string like "A-D"
// and returns an array like ['A','B','C','D']
$compareLetters = Helper::returnItemOrListAsArray($letter, true);
}
// Create the query
$query = $allDbConnections['composition']->createQueryBuilder();
// Collect constraints
$addConstraints = [];
// Compare first letter against given compareLetters
foreach($compareLetters as $l) {
$addConstraints[] = $query->expr()->like(
'composition.title',
$query->createNamedParameter($l . '%')
);
}
Trying to insert the resulting array like this:
$constraints = [
$query->expr()->eq(
'composition.sys_language_uid',
$query->createNamedParameter($language, \PDO::PARAM_INT)
),
$query->expr()->orX(
implode(',', $addConstraints)
)
]
throws an exception:
Operand should contain 1 column(s)
Currently I have no idea, how to do this differently nor how to interpret the exception. Any hint would be most welcome!
Not sure whether it's a good approach, but I would try out range() function to generate the query, something like this:
<?php
$userInput = 'A-D';
list($start, $end) = explode('-', $userInput);
$selection = [];
foreach (range($start, $end) as $letter) {
$selection[] = $query->expr()->like(
'composition.title',
$query->createNamedParameter($letter . '%')
);
}
$constraints = [
$query->expr()->eq(
'composition.sys_language_uid',
$query->createNamedParameter($language, \PDO::PARAM_INT)
),
$query->expr()->orX(...$selection)
];
So more or less what you already did. Just you are not using the spread operator in your example when calling orX()

Query giving double result instead of single

I have two tables: products and current_product_attribute_values
I have tried a join query to filter them as per attribute selected by the user but when I try this with an additional condition it gives me 2 results instead of one it is including the first one which is not matching as per query:
select * from `products` inner join `current_product_attribute_values` on `products`.`id` = `current_product_attribute_values`.`product_id` where `current_product_attribute_values`.`attribute_id` = ? or `current_product_attribute_values`.`attribute_value_id` = ? and `current_product_attribute_values`.`attribute_id` = ? or `current_product_attribute_values`.`attribute_value_id` = ? and `product_name` LIKE ?
here is my laravel Controller code :
$all = Input::all();
$q = Input::get('search_text');
$att_val = Input::get('attribute_value');
$subcat = Input::get('subcat_id');
$subcat_name = DB::table('subcategories')->where('id', $subcat)->value('subcategory_name');
$brandname = DB::table('brands')->where('subcat_id', $subcat)->value('brand_name');
$brand_id = DB::table('brands')->where('subcat_id', $subcat)->value('id');
$product_count = DB::table('products')->where('brand_id', $brand_id)->count();
if ($q != "") {
// getting multiple same name params
$query = DB::table('products');
$query->join('current_product_attribute_values', 'products.id', '=', 'current_product_attribute_values.product_id');
$j = 0;
foreach ($all as $key => $values) {
//echo 'my current get key is : ' . urldecode($key). '<br>';
if ($key == $name[$j]) {
$query->where('current_product_attribute_values.attribute_id', '=', $att_id_value[$j]);
echo'<br>';
print_r($query->toSql());
echo'<br>';
//echo '<br> key matched and have some value : <br>';
//echo count($values);
if (count($values) >= 1) {
//echo '<br> it has array inside <br>';
foreach ($values as $val) {
// or waali query in same attribute
echo'<br>';
$query->orwhere('current_product_attribute_values.attribute_value_id', '=', $val);
print_r($query->toSql());
echo'<br>';
}
}
$j++;
}
}
$records = $query->toSql();
$query->where('product_name', 'LIKE', '%' . $q . '%');
$records = $query->toSql();
print_r($records);
$products = $query->paginate(10)->setPath('');
$pagination = $products->appends(array(
'q' => Input::get('q')
));
if (count($products) > 0) {
$filters = DB::table('product_attributes')->where('subcategory_id', $subcat)->get(['attribute_title']);
} else {
$filters = array();
}
$categories = categories::where('add_to_menu', 1)->with('subcategories')->with('brands')->get();
$categoryhome = categories::where('add_to_menu', 1)->with('subcategories')->get();
return view('searchfilter')
->with('productsdata', $products)
->with('filtersdata', $filters)
->with('categories', $categories)
->with('categorieshome', $categoryhome)
->with('subcat_name', $subcat_name)
->with('subcat_id', $subcat)
->with('brandname', $brandname)
->with('product_count', $product_count)
->with('querytext', $q);
}
return 'No Details found. Try to search again !';
its easier if you use raw sql as calling db select function. ex:
$query=DB::select("select * from `products` inner join `current_product_attribute_values` on `products`.`id` = `current_product_attribute_values`.`product_id` where `current_product_attribute_values`.`attribute_id` = ? or `current_product_attribute_values`.`attribute_value_id` = ? and `current_product_attribute_values`.`attribute_id` = ? or `current_product_attribute_values`.`attribute_value_id` = ? and `product_name` LIKE ?
");
indeed you can concat vars in raw sql if you need to, ex:
$queryBrands = "select id from brands where subcat_id =".$subcat;
//echo $queryBrands
$queryBrands = DB::select($queryBrands);
By looking at your tables, product table with id value 17 has two records in table current_product_attribute_values in column product_id (I assume this column is used as foreign key to product table).
With select *, you select all of the columns from both tables. So it would most likely cause your query to return multiple records.
My suggestions:
Only select the columns you need. Avoid using select * in the long run, i.e. select product.id, product.description, current_product_attribute_values.attribute_values ......
Make use of GROUP BY
Hope these helps.

Extra, blank row from PDO select on Sqlite

This involves a Sqlite database, PHP 7 and PDO. The query code is:
...
$stmt = $pdo->query('SELECT * FROM images');
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)){
$images[] = [
"image_id" => $row["image_id"],
"date" => $row["date"],
"photographer" => $row["photographer"],
...
];
}
echo $stmt->rowCount() . " rows<br>";
echo count($images) . " images<br>";
var_dump($images);
return $images;
}
(Note: This is based on http://www.sqlitetutorial.net/sqlite-php/query/ . It will be revised soon to do prepared statements, enumerating cols, etc., once the problem described here is solved.)
The echos report "0 rows" and "2 images". The var_dump() outputs:
array(2) { [0]=> array(0) { } [1]=> array(14) { ["image_id"]=> ...
So clearly there's an extra, empty array in the first position in the outer array. In the calling code, which collects the $image array as return value, count($array) gives 2 not 1 (and code expecting name/value pairs in each row breaks).
The problem is, there's only one row in the table. This appears clearly on the command line: sqlite> select * from images; gets one row and:
sqlite> select count(*) as c from images;
1
What's wrong here?
Different array syntax solved it.
$stmt = $pdo->query('SELECT * FROM images');
$images = array();
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)){
$images[] = array(
"image_id" => $row["image_id"],
"date" => $row["date"],
"photographer" => $row["photographer"],
...
);
}
I'm still not clear on the reason, but this way avoids the anomalous empty row.

How can I tell if I'm at the last result when using WHILE so that I can omit a comma from my output?

I know I can do what I need to do by getting a total records count and if I'm at the last record, don't display a comma but there has to be a better way.
I'm trying to build an SQL statement programatically using values from MySQL.
The code:
$fql="SELECT ";
$result = mysql_query("SELECT field FROM fb_aa_fields WHERE fql_table = '$query'", $conn);
while ($row = mysql_fetch_array($result)){
$get_field = "".$row{'field'}."";
$fql = $fql."$get_field, ";
}
$fql = $fql."FROM ".$query." WHERE owner=".$get_uid."";
It outputs this:
SELECT aid, can_upload, cover_object_id, cover_pid, created, description, edit_link, link, location, modified, modified_major, name, object_id, owner, photo_count, size, type, video_count, visible, FROM album WHERE owner=522862206
The problem is the last comma between "visible" and "FROM". How would you suggest is the best way to make that comma go away?
It's less of a pain to detect whether you're at the first element than the last. You could do like
$i = 0;
while($row =...) {
if ($i++) $fql .= ',';
$fql .= $row['field'];
}
Or, possibly better, defer tacking on fields to the string til the end. There's a built-in function called implode, that you can use to insert the commas between them.
$fields = array();
while($row =...) {
$fields[] = $row['field'];
}
$fql .= implode(',', $fields);