[DBD::Pg 3/3] First pass at placeholder escpaing. In other words, allowing literal question marks to be passed into queries by adding a backslash to them. Affects other placeholders as well (dollarsign and colon) Add tests for ? and ?? by creating custom operators Adjust cleanup routines in the tests for the new operators Actual rewrite code in dbdimp.c modified from a proof of concept by Tim Bunce.

dbdpg-commits-gENoVmVU/[email protected] Fri, 2 Jan 2015 04:27:55 +0000
Newsgroups gmane.comp.db.postgresql.dbdpg.cvs
Message-ID <[email protected]>
Committed by Greg Sabino Mullane <greg-O9fzpki4YnJWk0Htik3J/[email protected]>

Subject: [DBD::Pg 3/3] First pass at placeholder escpaing. In other words,
allowing literal question marks to be passed into queries by adding a
backslash to them. Affects other placeholders as well (dollarsign and colon)
Add tests for ? and ?? by creating custom operators Adjust cleanup routines
in the tests for the new operators Actual rewrite code in dbdimp.c modified
from a proof of concept by Tim Bunce.

---
 dbdimp.c              | 17 ++++++++++++++++-
 t/12placeholders.t    | 31 +++++++++++++++++++++++++++++--
 t/dbdpg_test_setup.pl | 26 ++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 3 deletions(-)

diff --git a/dbdimp.c b/dbdimp.c
index d437d4d..c90d561 100644
--- a/dbdimp.c
+++ b/dbdimp.c
@@ -1733,7 +1733,7 @@ static void pg_st_split_statement (pTHX_ imp_sth_t * imp_sth, int version, char
 
 	if (TSTART_slow) TRC(DBILOGFP, "%sBegin pg_st_split_statement\n", THEADER_slow);
 	if (TRACE6_slow) TRC(DBILOGFP, "%spg_st_split_statement: (%s)\n", THEADER_slow, statement);
-	
+
 	/*
 	  If the pg_direct flag is set (or the string has no length), we do not split at all,
 	  but simply put everything verbatim into a single segment and return.
@@ -1979,6 +1979,21 @@ static void pg_st_split_statement (pTHX_ imp_sth_t * imp_sth, int version, char
 			continue;
 		}
 
+		/* If this placeholder is escaped, we rewrite the string to remove the
+		   backslash, and move on as if there is no placeholder */
+		if ('\\' == oldch) {
+			/* copy the placeholder-like character but ignore the backslash */
+			unsigned char *p = statement-2;
+			while(*p++) {
+				*(p-1) = *p;
+			}
+			/* We need to adjust these items because we just rewrote statement! */
+			statement--;
+			currpos--;
+			ch = *statement;
+			continue;
+		}
+
 		/* We might slurp in a placeholder, so mark the character before the current one */
 		/* In other words, inside of "ABC?", set sectionstop to point to "C" */
 		sectionstop=currpos-1;
diff --git a/t/12placeholders.t b/t/12placeholders.t
index eb4f3d2..cfd7164 100644
--- a/t/12placeholders.t
+++ b/t/12placeholders.t
@@ -17,7 +17,7 @@ my $dbh = connect_database();
 if (! $dbh) {
 	plan skip_all => 'Connection to database failed, cannot continue testing';
 }
-plan tests => 251;
+plan tests => 253;
 
 my $t='Connect to database for placeholder testing';
 isnt ($dbh, undef, $t);
@@ -841,10 +841,37 @@ while (my ($name,$res) = each %booltest) {
 	}
 }
 
+## Test of placeholder escaping. Enabled by default, so let's jump right in
+$t = q{Basic placeholder escaping works via backslash-question mark for \?};
+
+## But first, we need some operators
+$dbh->do('create operator ? (leftarg=int,rightarg=int,procedure=int4eq)');
+$dbh->commit();
+$dbh->do('create operator ?? (leftarg=text,rightarg=text,procedure=texteq)');
+$dbh->commit();
+
+$SQL = qq{SELECT count(*) FROM dbd_pg_test WHERE id \\? ?};
+$sth = $dbh->prepare($SQL);
+eval {
+	$count = $sth->execute(123);
+};
+is($@, '', $t);
+$sth->finish();
+
+$t = q{Basic placeholder escaping works via backslash-question mark for \?\?};
+$SQL = qq{SELECT count(*) FROM dbd_pg_test WHERE pname \\?\\? ?};
+$sth = $dbh->prepare($SQL);
+eval {
+	$count = $sth->execute('foobar');
+};
+is($@, '', $t);
+$sth->finish();
+
+## pg_placeholder_escaping = 1;
+
 ## Begin custom type testing
 
 $dbh->rollback();
 
 cleanup_database($dbh,'test');
 $dbh->disconnect();
-
diff --git a/t/dbdpg_test_setup.pl b/t/dbdpg_test_setup.pl
index 01fc98c..bebd833 100644
--- a/t/dbdpg_test_setup.pl
+++ b/t/dbdpg_test_setup.pl
@@ -22,6 +22,12 @@ my @matviews =
 	 'dbd_pg_matview',
      );
 
+my @operators =
+	(
+		'?.integer.integer',
+		'??.text.text',
+    );
+
 my @schemas =
 	(
 	 'dbd_pg_testschema',
@@ -792,6 +798,20 @@ sub relation_exists {
 } ## end of relation_exists
 
 
+sub operator_exists {
+
+	my ($dbh,$opname,$leftarg,$rightarg) = @_;
+
+	my $SQL = 'SELECT 1 FROM pg_catalog.pg_operator '.
+		'WHERE oprname=? AND oprleft::regtype::text = ? AND oprright::regtype::text = ?';
+	my $sth = $dbh->prepare_cached($SQL);
+	my $count = $sth->execute($opname,$leftarg,$rightarg);
+	$sth->finish();
+	return $count < 1 ? 0 : 1;
+
+} ## end of operator_exists
+
+
 sub cleanup_database {
 
 	## Clear out any testing objects in the current database
@@ -811,6 +831,12 @@ sub cleanup_database {
 		$dbh->do("DROP MATERIALIZED VIEW $schema.$name");
 	}
 
+	for my $name (@operators) {
+		my ($opname,$leftarg,$rightarg) = split /\./ => $name;
+		next if ! operator_exists($dbh,$opname,$leftarg,$rightarg);
+		$dbh->do("DROP OPERATOR $opname($leftarg,$rightarg)");
+	}
+
 	for my $name (@tables) {
 		my $schema = ($name =~ s/(.+)\.(.+)/$2/) ? $1 : $S;
 		next if ! relation_exists($dbh,$schema,$name);
-- 
1.8.4