Postgresql: PREPARE TRANSACTION - php

I've two DB servers db1 and db2.
db1 has a table called tbl_album
db2 has a table called tbl_user_album
CREATE TABLE tbl_album
(
id PRIMARY KEY,
name varchar(128)
...
);
CREATE TABLE tbl_user_album
(
id PRIMARY KEY,
album_id bigint
...
);
Now if a user wants to create an album what my php code needs to do is:
Create a record in db1 and save its id(primary key)
Create a record in db2 using it saved in first statement
Is it possible to keep these two statements in a transaction? I'm ok with a php solution too. I mean I'm fine if there is a solution that needs php code to retain db handles and commit or rollback on those handles.
Any help is much appreciated.

Yes it is possible, but do you really need it?
Think twice before you decide this really must be two separate databases.
You could just keep both connections open and ROLLBACK the first command if the second one fails.
If you'd really need prepared transactions, continue reading.
Regarding your schema - I would use sequence generators and RETURNING clause on database side, just for convenience.
CREATE TABLE tbl_album (
id serial PRIMARY KEY,
name varchar(128) UNIQUE,
...
);
CREATE TABLE tbl_user_album (
id serial PRIMARY KEY,
album_id bigint NOT NULL,
...
);
Now you will need some external glue - distributed transaction coordinator (?) - to make this work properly.
The trick is to use PREPARE TRANSACTION instead of COMMIT. Then after both transactions succeed, use COMMIT PREPARED.
PHP proof-of-concept is below.
WARNING! this code is missing the critical part - that is error control. Any error in $db2 should be caught and ROLLBACK PREPARED should be executed on $db1
If you don't catch errors you will leave $db1 with frozen transactions which is really, really bad.
<?php
$db1 = pg_connect( "dbname=db1" );
$db2 = pg_connect( "dbname=db2" );
$transid = uniqid();
pg_query( $db1, 'BEGIN' );
$result = pg_query( $db1, "INSERT INTO tbl_album(name) VALUES('Absolutely Free') RETURNING id" );
$row = pg_fetch_row($result);
$albumid = $row[0];
pg_query( $db1, "PREPARE TRANSACTION '$transid'" );
if ( pg_query( $db2, "INSERT INTO tbl_user_album(album_id) VALUES($albumid)" ) ) {
pg_query( $db1, "COMMIT PREPARED '$transid'" );
}
else {
pg_query( $db1, "ROLLBACK PREPARED '$transid'" );
}
?>
And again - think before you will use it. What Erwin proposes might be more sensible.
Oh and just one more note... To use this PostgreSQL feature, you need to set max_prepared_transactions config variable to nonzero value.

If you can access db2 from within db1, then you could optimize the process and actually keep it all inside a transaction. Use dblink or SQL MED for that.
If you roll back a transaction on the local server, what has been done via dblink on a remote server will not be rolled back. (That is one way to make changes persistent even if a transaction is rolled back.)
But you can execute code on the remote server that rolls back if not successful, and only execute it, if the operation in the local db has been successful first. If the remote operation fails you can roll back locally, too.
Also, use the RETURNING clause of INSERT to return id from a serial column.

It will be easier with PDO...
The main advantage of PDO is to capture errors (by PHP error line or returning SQL error messages) of each single SQL statment in the transaction.
See pdo.begintransaction, pdo.commit, pdo.rollback and pdo.error-handling.
Example:
$dbh->beginTransaction();
/* Do SQL */
$sth1 = $dbh->exec("CREATE TABLE tbl_album (..)");
$sth2 = $dbh->exec("CREATE TABLE tbl_user_album(..)");
/* Commit the changes */
$dbh->commit();

Related

How I can manually trigger the error "canceling statement due to conflict with recovery error" to my postgresql replciation scheme?

In order to test various settings into my postgresql hot standby replication schema I need to reproduce a situation where the following error:
SQLSTATE[40001]: Serialization failure: 7 ERROR: canceling statement due to conflict with recovery
DETAIL: User query might have needed to see row versions that must be removed.
Therefore, I try to make 2 processes 1 that updates forever a boolean field with its opposite and one that reads the value from the replica.
The update script is this one (loopUpdate.php):
$engine = 'pgsql';
$host = 'mydb.c3rrdbjxxkkk.eu-central-1.rds.amazonaws.com';
$database = 'dummydb';
$user = 'dummyusr';
$pass = 'dummypasswd';
$dns = $engine.':dbname='.$database.";host=".$host;
$pdo = new PDO($dns,$user,$pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
echo "Continious update a field on et_store in order to cause new row version.".PHP_EOL;
while(true)
{
$pdo->exec("UPDATE mytable SET boolval= NOT boolval where id=52");
}
And the read script is the following (./loopRead.php):
$engine = 'pgsql';
$host = 'mydb_replica.c3rrdbjxxkkk.eu-central-1.rds.amazonaws.com';
$database = 'dummydb';
$user = 'dummyusr';
$pass = 'dummypasswd';
$dns = $engine.':dbname='.$database.";host=".$host;
$pdo = new PDO($dns,$user,$pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
echo "Continious update a field on et_store in order to cause new row version.".PHP_EOL;
while(true)
{
$value=$pdo->exec("SELECT id, boolval FROM mytable WHERE id=52");
var_dump($value);
echo PHP_EOL;
}
And I execute them in parallel:
# From one shell session
$ php ./loopUpdate.php
# From another one shell session
$ php ./loopRead.php
The mydb_replica.c3rrdbjxxkkk.eu-central-1.rds.amazonaws.com is hot standby read replica of the mydb.c3rrdbjxxkkk.eu-central-1.rds.amazonaws.com.
But I fail to make the loopRead.php to fail with the error:
SQLSTATE[40001]: Serialization failure: 7 ERROR: canceling statement due to conflict with recovery
DETAIL: User query might have needed to see row versions that must be removed.
As far as I know the error I try to reproduce is because postgresql VACUUM action is performed during an active read transaction on read replica that asks rather stale data. So how I can cause my select statement to select on stale versions of my row?
On the standby, set max_standby_streaming_delay to 0 and hot_standby_feedback to off.
Then start a transaction on the standby:
SELECT *, pg_sleep(10) FROM atable;
Then DELETE rows from atable and VACUUM (VERBOSE) it on the primary server. Make sure some rows are removed.
Then you should be able to observe a replication conflict.
In order to cause your error you need to place a HUGE delay into your select query itself via a pg_delay postgresql function, therefore changing your query into:
SELECT id, boolval, pg_sleep(1000000000) FROM mytable WHERE id=52
So on a single transaction you have a "heavy" query and maximizes the chances of causing a PostgreSQL serialization error.
Though the detail will differ:
DETAIL: User was holding shared buffer pin for too long.
In tat case try to reduce the pg_delay value from 1000000000 into 10.

update two table in single query

i have two table in sql the 1st table is for account while the 2nd table is for testimonial . i am trying to update the two tables in single query. The update is successful if the account already have a testimonial but fails to update if the account has no testimonial yet .How can i fix this heres my code for the update ....
if(!$update=mysql_query(
"UPDATE
tblapplicant,
tbltestimonial
SET
tblapplicant.ImagePath='".$name."',
tbltestimonial.pic = '".$name."'
WHERE
tblapplicant.appid=tbltestimonial.appid"
)
)
1) You're working with a database, it defeats the purpose to use the same data being inserted into two different tables.
2) One gentleman also mentioned stop using MySQL... heres some reference code for you. Assuming you're using php.
3) If you want to use a single query to update 2 tables with the same info against recommendation. Use a stored procedure to update them both.
4) At which point are these account's interconnected in this query? I'm somehow intrigued if this system is in beta or testing?
With your "Where" conditions without matching a specific record, this will update every record that has a matching ID. This is highly not recommended until you add further conditions like username = .... or a condition that's specific to someone or a specific set of rows.
**I strongly advise you post the tables you're working with and what results you want achieve for the best advise. **
Can't really give a good consultation with you playing the whole overview close to the chest. Using this plain-Jane without further detail on what you're asking for is at your own risk my friend.
include/dbconnect.php optional recommended update
<?php
if (isset($mysqli)){
unset($mysqli);
}
define("HOST", "yo.ur.ip.addr"); // The host you want to connect to.
define("USER", "myfunctionalaccount"); // The database username.
define("PASSWORD", "superdoopersecurepassword!"); // The database password.
define("DATABASE", "thegoods");
$mysqli = new mysqli(HOST, USER, PASSWORD, DATABASE);
if ( $mysqli->connect_error ) {
die('Connect Error: ' . $mysqli->connect_error);
}
?>
functions.php <-- shouldn't be called functions if its going to be your form response
<?php
// SHOULD BE SOME MASSIVE LOGIC UP HERE FOR FORM DATA DECISIONING
include_once "include/dbconnect.php";
$name = addslashes($_FILES['image']['name']);
$image = mysql_real_escape_string(addslashes(file_get_contents($_FILES['image']['tmp_name'])));
if ($stmt = $mysqli->prepare("CALL UpdateTestimonials(?,?,?)"){
$stmt->bind_param($name, $image, $userid);
$stmt->execute();
// optional to show affected rows
$stmt->affected_rows
//
// use if you want to return values from DB
// $stmt->bind_result($result);
// $stmt->fetch;
}
$stmt->close
?>
MySQL build a stored procedure - fyi; definer is optional. Definer will allow you to run a query that only elevated privileges can access due to the safety of such a query. You can use create procedure w/o the definer parameter. dT is just an abbreviation for datatype. You would put varchar or int... etc..
use 'database';
DROP procedure if exists 'UpdateTestimonials';
DELIMITER $$
use 'UpdateTestimonials' $$
CREATE DEFINER='user'#'HOSTNAME/LOCALHOST/%' PROCEDURE 'mynewprocedure' (IN varINPUT varchar, IN varIMG blob, IN varAppID int)
BEGIN
UPDATE tblapplicant
SET imagepath = varINPUT,
pic = LOAD_FILE(varIMG)
WHERE appid = varAppID
END $$
DELIMITER;
Use LEFT JOIN:
if (!$update = mysql_query(
"UPDATE
tblapplicant
LEFT JOIN tbltestimonial ON tblapplicant.appid = tbltestimonial.appid
SET
tblapplicant.ImagePath = '" . $name . "',
tbltestimonial.pic = '" . $name . "'"
)
)
Also, if you need some additional filters for tbltestimonial, add them into LEFT JOIN condition
You could try with a transaction. BTW also please use prepared statements to prevent SQL Injection attacks.
<?php
// prefer mysqli over mysql. It is the more modern library.
$db = new mysqli("example.com", "user", "password", "database");
$db->autocommit(false); // begin a new transaction
// prepare statements
$update_applicant =
$db->prepare("UPDATE tblapplicant
SET tblapplicant.ImagePath = ?"));
$update_applicant->bind_param("s", $name));
$update_applicant->execute();
$update_testimonial =
$db->prepare("UPDATE tbltestimonial
SET tbltestimonial.pic = ?"));
$update_testimonial->bind_param("s", $name))
$update_testimonial->execute();
$db->commit(); // finish the whole transaction as successful,
// when everything has succeeded.
?>
Of course that would not create any testimonials, that don't exist. It just updates those, that do. When you want to insert new entries in tbltestimonial, do so explicitly with an INSERT statement inside the transaction.
MySQL does not fully support transactions. The tables have to use a table type, that can handle them, e.g. innodb. When that is the case, the transaction will make sure, that everyone else either sees all changes from the transactions, or none.
In many cases transactions allow you to perform a group of simple steps, that otherwise would need complex single queries or would not be possible without transactions at all.
Alternative Solution
Another approach of course would be an update-trigger. Create a trigger in your database, that fires whenever e.g. tblapplicant is updated and updates tbltestimonial accordingly. Then you don't have to care about that in your application code.

Update database php

mysql_query("UPDATE users SET `test` = '$unicornID' where id='$_SESSION[user_id]' ")
or die(mysql_error());
Now, when user clicks 'add to favorite'-button this line of code updates my database but also deletes all the old data from column test. What command should so that the old data is not deleted?
I think what you may be looking for is an "INSERT" sql query.
It would be something along the lines of;
"INSERT * INTO users WHERE test='$unicornID' and id='$_SESION['user_id']'";
Let me know how it goes. Cheers.
Another tip.
Use PDO with prepared statements:
$pdo = new PDO(sprintf('mysql:host=%s;dbname=%s', HOST, DATABASE), USER, PASSWORD);
And to insert something:
$params = array(':unicornID' => $unicornID, ':id' => $_SESSION['user_id']);
$stmt = $pdo->prepare("INSERT * INTO users WHERE test=:unicornID and id=:id");
$stmt->execute($params);
The old mysql(_query) commands are old and very vulnerable, PDO isn't as vulnerable.
The advantage of prepared statements are mainly that you can't inject via your variables some sql code.
Hope you understood my and my code
"Update" means that the old data row is changed; if you want to keep it, you have to insert a new one. In this case I think that you should copy the row (which may be done using "insert... select...") and then update the newly inserted line.

How does locking tables work?

I have a php script that will be requested several times "at the same time" I also have a field in a table let's call it persons as a flag for active/inactive. I want when the first instance of the script runs to set that field to inactive so that the rest instances will die when they check that field. Can someone provide a solution for that? How can I ensure that this script will run only once?
PHP, PDO, MySQL
Thank you very much in advance.
Your script should fetch the current flag within a transaction using a locking read, such as SELECT ... FOR UPDATE:
$dbh = new PDO("mysql:dbname=$dbname", $username, $password);
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
$dbh->beginTransaction();
// using SELECT ... FOR UPDATE, MySQL will hold all other connections
// at this point until the lock is released
$qry = $dbh->query('SELECT persons FROM my_table WHERE ... FOR UPDATE');
if ($qry->fetchColumn() == 'active') {
$dbh->query('UPDATE my_table SET persons = "inactive" WHERE ...');
$dbh->commit(); // releases lock so others can see they are inactive
// we are the only active connection
} else {
$dbh->rollBack();
// we are inactive
}
You can use MySQL's own 'named' locking functions without ever having to lock a table: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock
e.g. try get_lock('got here first', 0) with a 0 timeout. if you get a lock, you're first in the gate, and any subsequent requests will NOT get the lock and immediately abort.
however, be careful with this stuff. if you don't clean up after yourself and the client which gained the lock terminates abnormally, the lock will not be released and your "need locks for this" system is dead in the water until you manually clear the lock.

Running multiple queries with mysqli_multi_query and transactions

I'm developing an update system for a Web Application written in PHP. In the process of the update I might need to execute a bunch of MySQL scripts.
The basic process to run the scripts is:
Search for the Mysql scripts
Begin a transaction
Execute each script with mysqli_multi_query since a script can contain multiple queries
If everything goes ok COMMIT the transaction, otherwise ROLLBACK.
My code looks something like:
$link = mysqli_connect(...);
mysqli_autocommit($link, false);
// open dir and search for scripts in file.
// $file is an array with all the scripts
foreach ($scripts as $file) {
$script = trim(file_get_contents($scriptname));
if (mysqli_multi_query($link, $script)) {
while (mysqli_next_result($link)) {
if ($resSet = mysqli_store_result($link)) { mysqli_free_result($resSet); }
if (mysqli_more_results($link)) { }
}
}
// check for errors in any query of any script
if (mysqli_error($link)) {
mysqli_rollback($link);
return;
}
}
mysqli_commit($link);
Here is an example of the scripts (for demonstration purposes):
script.1.5.0.0.sql:
update `demo` set `alias` = 'test1' where `id` = 1;
update `users` set `alias` = 'user1' where `id` = 1;
script 1.5.1.0.sql:
insert into `users`(id, key, username) values(3, '100', 'column key does not exist');
insert into `users`(id, key, username) values(3, '1', 'column key exists');
In this case, script 1.5.0.0 would execute without errors and script 1.5.1.0 would generate an error (for demonstration purposes, let's say that column key is unique and there is already a row with key = 1).
In this case I want to rollback every query that was executed. But what happens is that the first insert of 1.5.1.0 is not in the database (correctly) but the updates from 1.5.0.0 were executed successfully.
Remarks:
My first option was to split every query from every script with ";" and execute the queries independently. This is not an option since I have to be able to insert HTML code to the database (ex: if I want to insert something like "& nbsp;")
I've already searched StackOverflow and google and came across solutions like this one but I would prefer using a solution like mysqli_multi_query rather than using a function to split every query. It's more understandable and easier for debug purposes
I haven't tested it, but I believe that I could merge all the scripts and execute just a query. However it would be usefull to execute one script at a time so that I can figure out which script has the error.
The tables engine is InnoDB.
Appreciate if you can point some way to make this work.
Edit:mysqli_multi_query() only returns false if the first query fails. If the first query doesn't fail then your code will run mysql_store_result() which if it succeeds will leave mysqli_error() empty. You need to check for errors after every mysqli function that can succeed or fail.
Ok, after spending another day debugging, i've discovered the problem.
Actually, it has nothing to do with the code itself or with mysqli functions. I'm used to MS SQL transactions which supports DDL statements. MySQL does not supports DDL statements and commits data implicitly (Implicit commit). I had one DROP Table in one of the scripts that was auto commiting data.

Categories