Why this PDO parametrized query behave "strangely"? - pdo

Here i have edited my original question.
I have alswo answered it, in my next message.
I'm trying to get results from MySQL with parametrized php PDO query, but thing behaves strangley. I dont know if it is a bug, or I am doing something wrong or not seeing something obvious.
Lets suppose there are these two tables in database
CREATE TABLE `users` (
`user_id` int(11) NOT NULL PRIMARY KEY
)
CREATE TABLE `users_contacts` (
`contact_id` int(11) NOT NULL PRIMARY KEY ,
`user_id` int(11) DEFAULT NULL,
`type` varchar(45) DEFAULT NULL,
`value` varchar(255) DEFAULT NULL
)
Fill them with minimal data :
INSERT INTO `users` (`user_id`) VALUES (125);
INSERT INTO `users_contacts` (`contact_id`, `user_id`, `type`, `value`)
VALUES(11432, 125, 'email', 'losleyTyped#offten.stinks'),
(11433, 125, 'phone', '1234567'),
(564, 125, 'unit', '910');
And then you try to fetch data like this
$db_name = "";
$db_user = "";
$db_pass = "";
$db_pdo = new pdo("mysql:host=localhost;dbname=$db_name","$db_user","$db_pass");
$user = 125;
$user_unit_btm = 900;
$user_unit_top = $user_unit_btm + 100;
$upload_user = $db_pdo -> prepare("SELECT K.value AS unit
FROM users AS O,
users_contacts AS K
WHERE O.user_id = :user_id AND
K.user_id = O.user_id AND
K.type = 'unit' AND
K.value >= :unit_btm AND
K.value < :unit_top
");
$upload_user -> execute( [":user_id" => $user,
":unit_btm" => $user_unit_btm,
":unit_top" => $user_unit_top
]
);
$upload_user = $upload_user -> fetch(PDO::FETCH_ASSOC);
var_dump($upload_user);
var_dump will return false, but there is no error(err is 0000)
I have reduced the problem, and find that only one parameter ":organization" is problematic and cause for bizare behevior.
But if you replace " K.value < :unit_top "
with variable $user_unit_top
" K.value < $user_unit_top "
Then, query returns result!
Same thing if i replace " K.value < :unit_top " with literal 1000,
" K.value < 100"
Then query returns result!
Why is this happening?

As mentioned in my comment to your answer.
The PHP documentation on PDOStatement::execute states.
An array of values with as many elements as there are bound parameters in the SQL statement being executed. All values are treated as PDO::PARAM_STR.
Source: https://www.php.net/manual/en/pdostatement.execute.php
Additionally PDOStatement::fetch() returns false when there are no more results or upon failure.
The return value of this function on success depends on the fetch type. In all cases, FALSE is returned on failure.
Example https://3v4l.org/NVECJ
$pdo = new \PDO('sqlite::memory:', null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$pdo->query('CREATE TABLE foo(id INTEGER)');
$stmt = $pdo->prepare('SELECT * FROM foo');
$stmt->execute();
var_dump($stmt->fetch());
//bool(false)
If you need to explicitly define a data type, other than PDO::PARAM_STR for the parameter being sent to MySQL, you would use PDOStatement::bindParam or PDOStatement::bindValue
Example:
$upload_user = $db_pdo->prepare('SELECT
K.value AS unit
FROM users AS O,
users_contacts AS K
WHERE O.user_id = :user_id
AND K.user_id = O.user_id
AND K.type = \'unit\'
AND K.value >= :unit_btm
AND K.value < :unit_top');
$upload_user->bindValue(':user_id', $user, PDO::PARAM_INT);
$upload_user->bindValue(':unit_btm', $user_unit_btm, PDO::PARAM_INT);
$upload_user->bindValue(':unit_top', $user_unit_top, PDO::PARAM_INT);
$upload_user->execute();
An alternative would be to force data type casting on the parameter in the query.
$upload_user = $db_pdo->prepare('SELECT
K.value AS unit
FROM users AS O,
users_contacts AS K
WHERE O.user_id = :user_id
AND K.user_id = O.user_id
AND K.type = \'unit\'
AND K.value >= (:unit_btm - 0)
AND K.value < (:unit_top - 0)'); //CAST(:unit_top AS SIGNED)
$upload_user->execute([
':user_id' => $user,
':unit_btm' => $user_unit_btm,
':unit_top' => $user_unit_top
]);
Another contributing factor to your issue, is that MySQL will perform an automatic conversion to the column's data type for the comparison. Where other RDMBS, like PostgreSQL and SQLite3 do not perform the same conversions.
When an operator is used with operands of different types, type
conversion occurs to make the operands compatible. Some conversions
occur implicitly. For example, MySQL automatically converts strings to
numbers as necessary, and vice versa.
Source: https://dev.mysql.com/doc/refman/5.7/en/type-conversion.html
Since your initial column data type was VARCHAR, this resulted in the following from your testing.DB Fiddle
Initial query as PDOStatement::execute([1000]).
SELECT IF('910' > '1000', 'fail', 'pass') AS if_str_to_str;
| if_str_to_str |
| ------------- |
| fail |
Manually supplying integer to the Query
SELECT IF('910' > 1000, 'fail', 'pass') AS if_str_to_int;
| if_str_to_int |
| ------------- |
| pass |
After changing the database column data type and using PDOStatement::execute([1000])
SELECT IF(910 > '1000', 'fail', 'pass') AS if_int_to_str;
| if_int_to_str |
| ------------- |
| pass |
Using PDOStatement::bindValue(':param', '1000', PDO::PARAM_INT) or ('1000' - 0)
SELECT IF('910' > CAST('1000' AS SIGNED), 'fail', 'pass') AS if_str_to_typecast_int;
| if_str_to_typecast_int |
| ---------------------- |
| pass |

Here is an answer if somebody else find him self with similar problem
Why is this happening?
I think it is because Php or MySQL automatic type recognition/casting
Take a look of the table users_contacts the column named 'value' is of type VARCHAR (because the table is something like EAV data model).
In prepared query from my question on this line
K.value < :unit_top
PHP/MySQL is supposing that parameter :unit_top is the same datatype as K.value column (VARCHAR) so it compares-collation them like strings ???, and the string "910" is later then "1000".
If you replace parameter ":unit_top" with variable $user_unit_top
K.value < $user_unit_top
or a literal
K.value < 1000
Then Php is doing numeric comparison, and "1000" is bigger then "910"
*If you change table users_contacts column named 'value' from VARCHAR to INT then the
K.value < :unit_top
comparison will be numeric
I think this is very inconsistent behavior, because if you do the following
$a = "1000";
$b = " 910";
var_dump($a < $b);
PHP will do automatic type casting and then numeric commparison, ...
Am I wrong ?

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

Create a module that implement a new fieldType in Drupal 8 with a form that containt a field image

I need create a new fieldType into a module, in the backoffice appear like exist the created fieldType, but in the fieldType I need set a two fields, a image and a url (like string), and I found a sql error when I create a new field from the current type in drupal backoffice, because I cannot found the correct file type for the image, or the way to do it.
I have the structure like this:
[module-name]/
+ src/
| + Plugin/
| | + Field/
| | | + FieldFormatter/
| | | + FieldType/
| | | + FieldWidget/
In the module file "FieldType/[module-name]Type.php" I have the function to create the schema:
/**
* {#inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
// $schema = parent::schema($field_definition);
$schema = [];
$schema['columns']['url_op'] = [
'description' => 'The url of the cropped image',
'type' => 'varchar',
'length' => 255,
];
$schema['columns']['image_op'] = [
'type' => 'managed_file',
'description' => 'The image to crope',
'upload_location' => 'public://openseadragon-int',
];
$schema['indexes']['image_op'] = ['image_op'];
return $schema;
}
The sql error is:
Ha habido un problema creando el campo Openseadragon:
Exception thrown while performing a schema update.
SQLSTATE[42000]: Syntax error or access violation: 1064
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'NULL DEFAULT NULL COMMENT 'The image to crope', PRIMARY KEY (`entity_id`, `dele' at line 9: CREATE TABLE {node__field_openseadragon} ( `bundle` VARCHAR(128) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '' COMMENT 'The field instance bundle to which this row belongs, used when deleting a field instance', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT 'A boolean indicating whether this data item has been deleted', `entity_id` INT unsigned NOT NULL COMMENT 'The entity id this data is attached to', `revision_id` INT unsigned NOT NULL COMMENT 'The entity revision id this data is attached to', `langcode` VARCHAR(32) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '' COMMENT 'The language code for this data item.', `delta` INT unsigned NOT NULL COMMENT 'The sequence number for this data item, used for multi-value fields', `field_openseadragon_url_op` VARCHAR(255) NULL DEFAULT NULL COMMENT 'The url of the cropped image', `field_openseadragon_image_op` NULL DEFAULT NULL COMMENT 'The image to crope', PRIMARY KEY (`entity_id`, `deleted`, `delta`, `langcode`), INDEX `bundle` (`bundle`), INDEX `revision_id` (`revision_id`), INDEX `field_openseadragon_image_op` (`field_openseadragon_image_op`) ) ENGINE = InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT 'Data storage for node field field_openseadragon.'; Array ( )

Relating ID's in relational table

I have these two insertion queries in Perl using the DBI module and DBD:mysql.
This one inserts fields url, html_extr_text, concord_file, and sys_time into table article:
my #fields = (qw(url html_extr_text concord_file sys_time));
my $fieldlist = join ", ", #fields;
my $field_placeholders = join ", ", map {'?'} #fields;
my $insert_query = qq{
INSERT INTO article ($fieldlist)
VALUES ($field_placeholders)
};
my $sth = $dbh->prepare($insert_query);
my $id_article;
my #id_articles;
foreach my $article_index (0 .. #output_concord_files_prepare) {
$field_placeholders = $sth->execute(
$url_prepare[$article_index],
$html_pages_files_extended[$article_index],
$output_concord_files_prepare[$article_index],
$sys_time_prepare[$article_index]);
$id_article = $dbh->last_insert_id(undef, undef, 'article', 'id_article');
push #id_articles, $id_article;
if ($field_placeholders != 1) {
die "Error inserting records, only [$field_placeholders] got inserted: " . $sth->insert->errstr;
}
}
print "#id_articles\n";
And this one inserts field event into table event:
#fields = (qw(event));
$fieldlist = join ", ", #fields;
$field_placeholders = join ", ", map {'?'} #fields;
$insert_query = qq{
INSERT INTO event ($fieldlist)
VALUES ($field_placeholders)
};
$sth = $dbh->prepare($insert_query);
my $id_event;
my #id_events;
foreach my $event_index (0 .. #event_prepare){
$field_placeholders = $sth->execute($event_prepare[$event_index]);
$id_event = $dbh->last_insert_id(undef, undef, 'event', 'id_event');
push #id_events, $id_event;
if ($field_placeholders != 1){
die "Error inserting records, only [$field_placeholders] got inserted: " . $sth->insert->errstr;
}
}
print "#id_events\n";
I'd like to create a third one-to-many relationship table. Because, one article contains multiple events, so I have this file :
output_concord/concord.0.txt -> earthquake
output_concord/concord.0.txt -> avalanche
output_concord/concord.0.txt -> snowfall
output_concord/concord.1.txt -> avalanche
output_concord/concord.1.txt -> rock fall
output_concord/concord.1.txt -> mud slide
output_concord/concord.4.txt -> avalanche
output_concord/concord.4.txt -> rochfall
output_concord/concord.4.txt -> topple
...
As you can see, I collect the IDs of each entry using the LAST_INSERT_ID. However I don't really know how to make the next step.
Using this file, how can I insert into a third table 'article_event_index' the ids of the two previous tables.
It would be something like this:
$create_query = qq{
create table article_event_index(
id_article int(10) NOT NULL,
id_event int(10) NOT NULL,
primary key (id_article, id_event),
foreign key (id_article) references article (id_article),
foreign key (id_event) references event (id_event)
)
};
$dbh->do($create_query);
Which will contain relationships following the pattern
1-1, 1-2, 1-3, 2-4, 3-5 ...
I'm a newbie to Perl and databases so it's hard to formulate what I want to do. I hope I was clear enough.
Something like this should do what you need (untested, but it does compile).
It starts by building Perl hashes to relate concord files to article IDs and events to event IDs. Then the file is read, and a pair of IDs is inserted into the new table for each relationship that can be found in the exisiting tables.
Note that the hashes are there only to avoid a long sequence of
SELECT id_article FROM article WHERE concord_file = ?
and
SELECT id_event FROM event WHERE event = ?
statements.
use strict;
use warnings;
use DBI;
use constant RELATIONSHIP_FILE => 'relationships.txt';
my $dbh = DBI->connect('DBI:mysql:database', 'user', 'pass')
or die $DBI::errstr;
$dbh->do('DROP TABLE IF EXISTS article_event_index');
$dbh->do(<< 'END_SQL');
CREATE TABLE article_event_index (
id_article INT(10) NOT NULL,
id_event INT(10) NOT NULL,
PRIMARY KEY (id_article, id_event),
FOREIGN KEY (id_article) REFERENCES article (id_article),
FOREIGN KEY (id_event) REFERENCES event (id_event)
)
END_SQL
my $articles = $dbh->selectall_hashref(
'SELECT id_article, concord_file FROM article',
'concord_file'
);
my $events = $dbh->selectall_hashref(
'SELECT id_event, event FROM event',
'event'
);
open my $fh, '<', RELATIONSHIP_FILE
or die sprintf qq{Unable to open "%s": %s}, RELATIONSHIP_FILE, $!;
my $insert_sth = $dbh->prepare('INSERT INTO article_event_index (id_article, id_event) VALUES (?, ?)');
while (<$fh>) {
chomp;
my ($concord_file, $event) = split /\s*->\s*/;
next unless defined $event;
unless (exists $articles->{$concord_file}) {
warn qq{No article record for concord file "$concord_file"};
next;
}
my $id_article = $articles->{$concord_file}{id_article};
unless (exists $events->{$event}) {
warn qq{No event record for event "$event"};
next;
}
my $id_event = $events->{$event}{id_event};
$insert_sth->execute($id_article, $id_event);
}

How to add whole array to one field in database

I have tried everything, array_push, multidimensional array and so on and nothing worked for me.
Following situation:
try {
if (isset($_SESSION['list']) > 0) {
foreach($_SESSION['list'] as $id=> $quantity) {
$sQuery = "SELECT * FROM table WHERE id = '".$id."' ";
$oStmt = $db->prepare($sQuery);
$oStmt->execute();
while($aRow = $oStmt->fetch(PDO::FETCH_ASSOC)) {
$id = $aRow['id'];
$name = $aRow['name'];
$volume = $aRow['volume'];
}
$testar = array(array('name' => $name, 'volume' => $volume, 'quantity' => $quantity));
$sQuery = "INSERT INTO table (array_data, date) VALUES ('$testar', NOW())";
$oStmt = $db->prepare($sQuery);
$oStmt->execute();
print_r($testar);
}
}
else {
echo 'Nothing to add';
}
}
catch(PDOException $e) {
$sMsg = '<p>
Regelnummer: '.$e->getLine().'<br />
Bestand: '.$e->getFile().'<br />
Foutmelding: '.$e->getMessage().'
</p>';
trigger_error($sMsg);
}
when I print_r($testar); I get this:
Array ( [0] => Array ( [name] => test 1 [volume] => 1.50 [quantity] => 4 ) ) Array ( [0] => Array ( [name] => test 2 [volume] => 2.50 [quantity] => 5 ) ) Array ( [0] => Array ( [name] => test 3 [volume] => 2.50 [quantity] => 2 ) )
but when I add it to database I only see: ARRAY.
How is that possible?
What I want is to add the whole Array to one field in database. Is that possible and how can I arrange that?
Usually, a column in an SQL database should have only one value, not an array. If you have multiple values, you should values individually, either as separate columns if they are a set of totally different types of data, or else as a single column on multiple rows of a dependent table if the array is multiple values of the same type of data.
This rule comes from First Normal Form.
But if you really need to store a PHP array in one row, you can convert the PHP array into a string with PHP's serialize() function. This is better than implode() because serialize() preserves hash keys, arrays of arrays, etc.
$testar = array(array('name' => $name, 'volume' => $volume, 'quantity' => $quantity));
$testar_serialized = serialize($testar);
$sQuery = "INSERT INTO table (array_data, date) VALUES (?, NOW())";
$oStmt = $db->prepare($sQuery);
$oStmt->execute( array($testar_serialized) );
I would start looking at the implode function.
Than you can add all the elements in the array to the field in the database, represented as a string.
A few things:
You don't insert an array into an SQL table; you insert the values of it.
Your SQL table should have a "column" for each field (e.g. name, volume, quantity), and will get a "row" for each entry in the outer array.
You should not have a variable reference in your SQL. This is called an “SQL injection vector,” and (due to accident or malice) will eventually wreak havoc upon you.
What you should do where your INSERT is, is:
prepare your SQL statement once:
INSERT INTO table (name, volume, quantity) VALUES (?,?,?);
The ? mark where the SQL drivers will place your values, which you specify later.
This tells the database to get ready to accept (1 or more) inserts in this form, and
shows the database driver (PDO, mysqli, …) where to put the values when you later
call execute
For each row in the array:
call execute with the list of values, e.g. ->execute ($testar->[$i]->['name'], …

How To Split Pipe-Delimited Column and insert each value into new table Once?

I have an old database with a gazillion records (more or less) that have a single tags column (with tags being pipe-delimited) that looks like so:
Breakfast
Breakfast|Brunch|Buffet|Burger|Cakes|Crepes|Deli|Dessert|Dim Sum|Fast Food|Fine Wine|Spirits|Kebab|Noodles|Organic|Pizza|Salad|Seafood|Steakhouse|Sushi|Tapas|Vegetarian
Breakfast|Brunch|Buffet|Burger|Deli|Dessert|Fast Food|Fine Wine|Spirits|Noodles|Pizza|Salad|Seafood|Steakhouse|Vegetarian
Breakfast|Brunch|Buffet|Cakes|Crepes|Dessert|Fine Wine|Spirits|Salad|Seafood|Steakhouse|Tapas|Teahouse
Breakfast|Brunch|Burger|Crepes|Salad
Breakfast|Brunch|Cakes|Dessert|Dim Sum|Noodles|Pizza|Salad|Seafood|Steakhouse|Vegetarian
Breakfast|Brunch|Cakes|Dessert|Dim Sum|Noodles|Pizza|Salad|Seafood|Vegetarian
Breakfast|Brunch|Deli|Dessert|Organic|Salad
Breakfast|Brunch|Dessert|Dim Sum|Hot Pot|Seafood
Breakfast|Brunch|Dessert|Dim Sum|Seafood
Breakfast|Brunch|Dessert|Fine Wine|Spirits|Noodles|Pizza|Salad|Seafood
Breakfast|Brunch|Dessert|Fine Wine|Spirits|Salad|Vegetarian
Is there a way one could retrieve each tag and insert it into a new table tag_id | tag_nm using MySQL only?
Here is my attempt which uses PHP..., I imagine this could be more efficient with a clever MySQL query. I've placed the relationship part of it there too. There's no escaping and error checking.
$rs = mysql_query('SELECT `venue_id`, `tag` FROM `venue` AS a');
while ($row = mysql_fetch_array($rs)) {
$tag_array = explode('|',$row['tag']);
$venueid = $row['venue_id'];
foreach ($tag_array as $tag) {
$rs2 = mysql_query("SELECT `tag_id` FROM `tag` WHERE tag_nm = '$tag'");
$tagid = 0;
while ($row2 = mysql_fetch_array($rs2)) $tagid = $row2['tag_id'];
if (!$tagid) {
mysql_execute("INSERT INTO `tag` (`tag_nm`) VALUES ('$tag')");
$tagid = mysql_insert_id;
}
mysql_execute("INSERT INTO `venue_tag_rel` (`venue_id`, `tag_id`) VALUES ($venueid, $tagid)");
}
}
After finding there is no official split function I've solved the issue using only MySQL like so:
1: I created the function strSplit
CREATE FUNCTION strSplit(x varchar(21845), delim varchar(255), pos int) returns varchar(255)
return replace(
replace(
substring_index(x, delim, pos),
substring_index(x, delim, pos - 1),
''
),
delim,
''
);
Second I inserted the new tags into my new table (real names and collumns changed, to keep it simple)
INSERT IGNORE INTO tag (SELECT null, strSplit(`Tag`,'|',1) AS T FROM `old_venue` GROUP BY T)
Rinse and repeat increasing the pos by one for each collumn (in this case I had a maximum of 8 seperators)
Third to get the relationship
INSERT INTO `venue_tag_rel`
(Select a.`venue_id`, b.`tag_id` from `old_venue` a, `tag` b
WHERE
(
a.`Tag` LIKE CONCAT('%|',b.`tag_nm`)
OR a.`Tag` LIKE CONCAT(b.`tag_nm`,'|%')
OR a.`Tag` LIKE CONCAT(CONCAT('%|',b.`tag_nm`),'|%')
OR a.`Tag` LIKE b.`tag_nm`
)
)