Rose::DB masking in query statements - sql

I'm seeking some help on my Rose::DB issue as described below.
I have an account object, and this has an integer "accounttype" field. In this example I will use the following accounttype constants:
ACCOUNT_TYPE_SPECIAL_MASK = 0x10;
ACCOUNT_TYPE_SPECIAL_1 = 0x10;
ACCOUNT_TYPE_SPECIAL_2 = 0x11;
ACCOUNT_TYPE_NORMAL_MASK = 0x20;
ACCOUNT_TYPE_NORMAL_1 = 0x20;
ACCOUNT_TYPE_NORMAL_2 = 0x21;
At present, when I want accounts of a given type, I'd list them all and do something like this:
my $iter = Test::Account::Manager->get_accounts_iterator(
db => $db,
query =>
[
'accounttype' => [ ACCOUNT_TYPE_SPECIAL_1, ACCOUNT_TYPE_SPECIAL_2 ],
]
);
However, I'd like to be able to query for the accounts using the appropriate mask, rather than specify all possible types.
I'd like to be able to say:
my $iter = Test::Account::Manager->get_accounts_iterator(
db => $db,
query =>
[
'accounttype' => 'accounttype & ACCOUNT_TYPE_SPECIAL_MASK'
]
);
However, I haven't spotted any way to do this. Any help or recommendations most welcome.
Thanks!

Let's say your SQL server understands the following:
(accounttype & 16) <> 0
This would then suggest that you could use the following:
[ \'(accounttype & ?) <> 0' => ACCOUNT_TYPE_SPECIAL_MASK ]
Under the circumstances, you could inline the constant.
\sprintf('(accounttype & %d) <> 0', ACCOUNT_TYPE_SPECIAL_MASK)
I don't know if these two versions result in different SQL, and I don't know which is faster if so.

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()

Perl SQL::Parser table alias substitution: works for SELECT column names but not for WHERE column names

I'm trying to parse some SQL queries stored in a log database -- I don't want to submit them to a SQL database, just to extract the fields used in the SELECT and WHERE clause.
I've been fiddling with several SQL parsers in Java, Python and Perl. The one that seems to work better for my problem are SQL::Parser and SQL::Statement. With those I was able to write the following code:
#!/usr/bin/perl
use strict;
use SQL::Parser;
use SQL::Statement;
use Data::Dumper;
my $sql = "SELECT sl.plate,sp.fehadop FROM sppLines AS sl ".
"JOIN sppParams AS sp ON sl.specobjid = sp.specobjid ".
"WHERE fehadop < -3.5 ";
my $parser = SQL::Parser->new();
my $stmt = SQL::Statement->new($sql,$parser);
printf("COMMAND [%s]\n",$stmt->command);
printf("COLUMNS \n");
my #columns = #{$stmt->column_defs()};
foreach my $column ( #columns)
{
print " ".$column->{value}."\n";
}
printf("TABLES \n");
my #tables = $stmt->tables();
foreach my $table ( #tables)
{
print " ".$table->{name}."\n";
}
printf("WHERE COLUMNS\n");
my $where_hash = $stmt->where_hash();
print Dumper($where_hash);
Sorry if it is too long, it is the smallest, self-contained example I could devise.
The output of this code is:
COMMAND [SELECT]
COLUMNS
spplines.plate
sppparams.fehadop
TABLES
spplines
sppparams
WHERE COLUMNS
$VAR1 = {
'arg1' => {
'value' => 'fehadop',
'type' => 'column',
'fullorg' => 'fehadop'
},
'op' => '<',
'nots' => {},
'arg2' => {
'str' => '-?0?',
'fullorg' => '-3.5',
'name' => 'numeric_exp',
'value' => [
{
'fullorg' => '3.5',
'value' => '3.5',
'type' => 'number'
}
],
'type' => 'function'
},
'neg' => 0
};
The parser returns the name of columns (obtained through a call to $stmt->column_defs()) already renamed with the real tables names (e.g. spplines.plate instead of s1.plate) -- this is what I want.
I also want the names of the columns used in the WHERE clause.
I already know how to recursively parse the results of $stmt->where_hash() (didn't include the code to make the post clear), but even from dumping its contents I can see that the column names are not associated with the tables.
I would like to ensure that the columns names in the WHERE clause are also preceded by the tables name. After parsing the results of $stmt->where_hash() I would get sppparams.fehadop instead of fehadop.
Is this possible with SQL::Parser?
Thanks
(big edit -- tried to make the question clearer)
Since SQL::Statement has an eval_where, I suspect there might be a better way, but you can try a function like this:
get_column($stmt->column_defs(), $where_hash->{arg1});
sub get_column {
my ($columns, $arg) = #_;
return $arg->{fullorg} if ($arg->{type} ne 'column');
foreach my $col (#$columns) {
return $col->{value} if ($col->{fullorg} eq $arg->{fullorg});
my ($name) = ( $col->{fullorg} =~ /([^.]+)$/);
return $col->{value} if ($name eq $arg->{fullorg});
}
return $arg->{fullorg};
}

Node-postgres: named parameters query (nodejs)

I used to name my parameters in my SQL query when preparing it for practical reasons like in php with PDO.
So can I use named parameters with node-postgres module?
For now, I saw many examples and docs on internet showing queries like so:
client.query("SELECT * FROM foo WHERE id = $1 AND color = $2", [22, 'blue']);
But is this also correct?
client.query("SELECT * FROM foo WHERE id = :id AND color = :color", {id: 22, color: 'blue'});
or this
client.query("SELECT * FROM foo WHERE id = ? AND color = ?", [22, 'blue']);
I'm asking this because of the numbered parameter $n that doesn't help me in the case of queries built dynamically.
There is a library for what you are trying to do. Here's how:
var sql = require('yesql').pg
client.query(sql("SELECT * FROM foo WHERE id = :id AND color = :color")({id: 22, color: 'blue'}));
QueryConvert to the rescue. It will take a parameterized sql string and an object and converts it to pg conforming query config.
type QueryReducerArray = [string, any[], number];
export function queryConvert(parameterizedSql: string, params: Dict<any>) {
const [text, values] = Object.entries(params).reduce(
([sql, array, index], [key, value]) => [sql.replace(`:${key}`, `$${index}`), [...array, value], index + 1] as QueryReducerArray,
[parameterizedSql, [], 1] as QueryReducerArray
);
return { text, values };
}
Usage would be as follows:
client.query(queryConvert("SELECT * FROM foo WHERE id = :id AND color = :color", {id: 22, color: 'blue'}));
Not exactly what the OP is asking for. But you could also use:
import SQL from 'sql-template-strings';
client.query(SQL`SELECT * FROM unicorn WHERE color = ${colorName}`)
It uses tag functions in combination with template literals to embed the values
I have been working with nodejs and postgres. I usually execute queries like this:
client.query("DELETE FROM vehiculo WHERE vehiculo_id= $1", [id], function (err, result){ //Delete a record in de db
if(err){
client.end();//Close de data base conection
//Error code here
}
else{
client.end();
//Some code here
}
});

SQL Compare Characters in two strings count total identical

So the over all on this is I have two different systems and in both systems I have customers, unfortunately both systems allow you to type in the business name freehand so you end up with the example below.
Column A has a value of "St John Baptist Church"
Column B has a value of "John Baptist St Church"
What I need to come up with is a query that can compare the two columns to find the most closely matched values.
From there I plan to write a web app where I can have someone go through and validate all of the entries. I would enter in some example of what I have done, but unfortunately I honestly dont even know if what I am asking for is even possible. I would think it is though in this day and age I am sure I am not the first one to try to attempt this.
You could try and create a script something like this php script to help you:
$words = array();
$duplicates = array();
function _compare($value, $key, $array) {
global $duplicates;
$diff = array_diff($array, $value);
if (!empty($diff)) {
$duplicates[$key] = array_keys($diff);
}
return $diff;
}
$mysqli = new mysqli('localhost', 'username', 'password', 'database');
$query = "SELECT id, business_name FROM table";
if ($result = $mysqli->query($query)) {
while ($row = $result->fetch_object()) {
$pattern = '#[^\w\s]+#i';
$row->business_name = preg_replace($pattern, '', $row->business_name);
$_words = explode(' ', $row->business_name);
$diff = array_walk($words, '_compare', $_words);
$words[$row->id][] = $_words;
$result->close();
}
}
$mysqli->close();
This is not tested but you need something like this, because I don't think this is possible with SQL alone.
---------- EDIT ----------
Or you could do a research on what the guys in the comment recommend Levenshtein distance in T-SQL
Hope it helps, good luck!