From 2422918a24b4e50bffbfde9addd3393f9f22f9a8 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Tue, 15 Aug 2023 17:19:01 +0100 Subject: [PATCH 1/8] feature: implement dynamic value set closes #355 --- src/Query/SqlQuery.php | 43 +++++++++++++++++++++++++++++ test/phpunit/Query/SqlQueryTest.php | 35 +++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index e07303f..61b46e9 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -117,6 +117,49 @@ public function injectSpecialBindings( return $sql; } + public function injectDynamicBindings(string $sql, array &$data):string { + $sql = $this->injectDynamicBindingsValueset($sql, $data); + return $sql; + } + + private function injectDynamicBindingsValueset($sql, &$data):string { + $pattern = '/\(\s*:__dynamicValues\s\)/'; + if(!preg_match($pattern, $sql, $matches)) { + return $sql; + } + + $replacementRowList = []; + $originalKeys = array_keys($data[0]); + foreach($data as $i => $kvp) { + $indexedRow = []; + foreach($kvp as $key => $value) { + $indexedKey = $key . "_" . str_pad($i, 5, "0", STR_PAD_LEFT); + array_push($indexedRow, $indexedKey); + + $data[$indexedKey] = $value; + } + unset($data[$i]); + array_push($replacementRowList, $indexedRow); + } + + $replacementString = ""; + foreach($replacementRowList as $i => $indexedKeyList) { + if($i > 0) { + $replacementString .= ",\n"; + } + $replacementString .= "("; + foreach($indexedKeyList as $j => $key) { + if($j > 0) { + $replacementString .= ","; + } + $replacementString .= "\n\t:$key"; + } + $replacementString .= "\n)"; + } + + return str_replace($matches[0], $replacementString, $sql); + } + /** * @param array|array $bindings * @return array|array diff --git a/test/phpunit/Query/SqlQueryTest.php b/test/phpunit/Query/SqlQueryTest.php index f792c95..8deb9b9 100644 --- a/test/phpunit/Query/SqlQueryTest.php +++ b/test/phpunit/Query/SqlQueryTest.php @@ -301,6 +301,41 @@ public function testSpecialBindingsInClause( ); } + /** + * @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider() + */ + public function testDynamicBindingsInsertMultiple( + string $queryName, + string $queryCollectionPath, + string $filePath + ) { + $sql = "insert into test_table (id, name) values ( :__dynamicValueSet )"; + file_put_contents($filePath, $sql); + $query = new SqlQuery($filePath, $this->driverSingleton()); + $data = [ + ["id" => 100, "name" => "first inserted"], + ["id" => 101, "name" => "second inserted"], + ["id" => 102, "name" => "third inserted"], + ]; + $originalData = $data; + $injectedSql = $query->injectDynamicBindings($sql, $data); + + self::assertStringNotContainsString("dynamicFieldset", $injectedSql); + + self::assertStringContainsString(":id_00000", $injectedSql); + self::assertStringContainsString(":id_00001", $injectedSql); + self::assertStringContainsString(":id_00002", $injectedSql); + self::assertStringContainsString(":name_00000", $injectedSql); + self::assertStringContainsString(":name_00001", $injectedSql); + self::assertStringContainsString(":name_00002", $injectedSql); + + foreach($originalData as $i => $kvp) { + foreach($kvp as $key => $value) { + $indexedKey = $key . "_" . str_pad($i, 5, "0", STR_PAD_LEFT); + self::assertSame($data[$indexedKey], $value); + } + } + } /** * @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider() */ From 0492885f74040e0b27678afd3d3ec6c1405bc3af Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Tue, 15 Aug 2023 17:25:41 +0100 Subject: [PATCH 2/8] tweak: dynamic value set syntax --- src/Query/SqlQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index 61b46e9..8949690 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -123,7 +123,7 @@ public function injectDynamicBindings(string $sql, array &$data):string { } private function injectDynamicBindingsValueset($sql, &$data):string { - $pattern = '/\(\s*:__dynamicValues\s\)/'; + $pattern = '/\(\s*:__dynamicValueSet\s\)/'; if(!preg_match($pattern, $sql, $matches)) { return $sql; } From 8fd2602bb2121fa968657b1ee69e68c0c1906c4e Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Tue, 15 Aug 2023 17:37:55 +0100 Subject: [PATCH 3/8] feature: implement dynamic in closes #347 --- src/Query/SqlQuery.php | 26 +++++++++++++++++--- test/phpunit/Query/SqlQueryTest.php | 38 ++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index 8949690..e81cc05 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -118,19 +118,22 @@ public function injectSpecialBindings( } public function injectDynamicBindings(string $sql, array &$data):string { - $sql = $this->injectDynamicBindingsValueset($sql, $data); + $sql = $this->injectDynamicBindingsValueSet($sql, $data); + $sql = $this->injectDynamicIn($sql, $data); return $sql; } - private function injectDynamicBindingsValueset($sql, &$data):string { + private function injectDynamicBindingsValueSet(string $sql, array &$data):string { $pattern = '/\(\s*:__dynamicValueSet\s\)/'; if(!preg_match($pattern, $sql, $matches)) { return $sql; } + if(!isset($data["__dynamicValueSet"])) { + return $sql; + } $replacementRowList = []; - $originalKeys = array_keys($data[0]); - foreach($data as $i => $kvp) { + foreach($data["__dynamicValueSet"] as $i => $kvp) { $indexedRow = []; foreach($kvp as $key => $value) { $indexedKey = $key . "_" . str_pad($i, 5, "0", STR_PAD_LEFT); @@ -141,6 +144,7 @@ private function injectDynamicBindingsValueset($sql, &$data):string { unset($data[$i]); array_push($replacementRowList, $indexedRow); } + unset($data["__dynamicValueSet"]); $replacementString = ""; foreach($replacementRowList as $i => $indexedKeyList) { @@ -160,6 +164,20 @@ private function injectDynamicBindingsValueset($sql, &$data):string { return str_replace($matches[0], $replacementString, $sql); } + private function injectDynamicIn(string $sql, array &$data):string { + $pattern = '/\(\s*:__dynamicIn\s\)/'; + if(!preg_match($pattern, $sql, $matches)) { + return $sql; + } + if(!isset($data["__dynamicIn"])) { + return $sql; + } + + $replacementString = implode(", ", $data["__dynamicIn"]); + unset($data["__dynamicIn"]); + return str_replace($matches[0], "( $replacementString )", $sql); + } + /** * @param array|array $bindings * @return array|array diff --git a/test/phpunit/Query/SqlQueryTest.php b/test/phpunit/Query/SqlQueryTest.php index 8deb9b9..4aa5831 100644 --- a/test/phpunit/Query/SqlQueryTest.php +++ b/test/phpunit/Query/SqlQueryTest.php @@ -313,9 +313,11 @@ public function testDynamicBindingsInsertMultiple( file_put_contents($filePath, $sql); $query = new SqlQuery($filePath, $this->driverSingleton()); $data = [ - ["id" => 100, "name" => "first inserted"], - ["id" => 101, "name" => "second inserted"], - ["id" => 102, "name" => "third inserted"], + "__dynamicValueSet" => [ + ["id" => 100, "name" => "first inserted"], + ["id" => 101, "name" => "second inserted"], + ["id" => 102, "name" => "third inserted"], + ], ]; $originalData = $data; $injectedSql = $query->injectDynamicBindings($sql, $data); @@ -329,13 +331,41 @@ public function testDynamicBindingsInsertMultiple( self::assertStringContainsString(":name_00001", $injectedSql); self::assertStringContainsString(":name_00002", $injectedSql); - foreach($originalData as $i => $kvp) { + foreach($originalData["__dynamicValueSet"] as $i => $kvp) { foreach($kvp as $key => $value) { $indexedKey = $key . "_" . str_pad($i, 5, "0", STR_PAD_LEFT); self::assertSame($data[$indexedKey], $value); } } + + self::assertArrayNotHasKey("__dynamicValueSet", $data); + } + + /** + * @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider() + */ + public function testDynamicBindingsWhereIn( + string $queryName, + string $queryCollectionPath, + string $filePath + ) { + $sql = "select `id`, `name` from `test_table` where `createdAt` > :startDate and `id` in ( :__dynamicIn ) limit 10"; + file_put_contents($filePath, $sql); + $query = new SqlQuery($filePath, $this->driverSingleton()); + $data = [ + "startDate" => "2020-01-01", + "__dynamicIn" => [1, 2, 3, 4, 5, 50, 60, 70, 80, 90], + ]; + $originalData = $data; + $injectedSql = $query->injectDynamicBindings($sql, $data); + + self::assertStringNotContainsString("dynamicIn", $injectedSql); + + self::assertStringContainsString("where `createdAt` > :startDate and `id` in ( 1, 2, 3, 4, 5, 50, 60, 70, 80, 90 ) limit 10", $injectedSql); + self::assertArrayNotHasKey("__dynamicIn", $data); + self::assertSame("2020-01-01", $data["startDate"]); } + /** * @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider() */ From 725ddbaa558e8dfa2e0043f087a1aed1dd97b030 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Tue, 15 Aug 2023 23:04:24 +0100 Subject: [PATCH 4/8] tidy: dynamic injection --- src/Query/SqlQuery.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index e81cc05..b7812b9 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -16,12 +16,17 @@ class SqlQuery extends Query { ]; /** @param array|array $bindings */ - public function getSql(array $bindings = []):string { + public function getSql(array &$bindings = []):string { $sql = file_get_contents($this->getFilePath()); - return $this->injectSpecialBindings( + $sql = $this->injectDynamicBindings( $sql, $bindings ); + $sql = $this->injectSpecialBindings( + $sql, + $bindings + ); + return $sql; } /** @param array|array $bindings */ @@ -117,12 +122,13 @@ public function injectSpecialBindings( return $sql; } + /** @param array> $data */ public function injectDynamicBindings(string $sql, array &$data):string { $sql = $this->injectDynamicBindingsValueSet($sql, $data); - $sql = $this->injectDynamicIn($sql, $data); - return $sql; + return $this->injectDynamicIn($sql, $data); } + /** @param array> $data */ private function injectDynamicBindingsValueSet(string $sql, array &$data):string { $pattern = '/\(\s*:__dynamicValueSet\s\)/'; if(!preg_match($pattern, $sql, $matches)) { @@ -164,6 +170,7 @@ private function injectDynamicBindingsValueSet(string $sql, array &$data):string return str_replace($matches[0], $replacementString, $sql); } + /** @param array> $data */ private function injectDynamicIn(string $sql, array &$data):string { $pattern = '/\(\s*:__dynamicIn\s\)/'; if(!preg_match($pattern, $sql, $matches)) { From 9311eae362b8bb29f7ae4a0f46a04ab1c415ba52 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Thu, 17 Aug 2023 21:58:38 +0100 Subject: [PATCH 5/8] wip: automatically wrap strings in single quotes --- src/Query/SqlQuery.php | 7 +++++++ test/phpunit/Query/SqlQueryTest.php | 27 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index b7812b9..ea4364f 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -180,6 +180,13 @@ private function injectDynamicIn(string $sql, array &$data):string { return $sql; } + foreach($data["__dynamicIn"] as $i => $value) { + if(is_string($value)) { + $value = str_replace("'", "\'", $value); + $data["__dynamicIn"][$i] = "'$value'"; + } + } + $replacementString = implode(", ", $data["__dynamicIn"]); unset($data["__dynamicIn"]); return str_replace($matches[0], "( $replacementString )", $sql); diff --git a/test/phpunit/Query/SqlQueryTest.php b/test/phpunit/Query/SqlQueryTest.php index 4aa5831..917f658 100644 --- a/test/phpunit/Query/SqlQueryTest.php +++ b/test/phpunit/Query/SqlQueryTest.php @@ -309,7 +309,7 @@ public function testDynamicBindingsInsertMultiple( string $queryCollectionPath, string $filePath ) { - $sql = "insert into test_table (id, name) values ( :__dynamicValueSet )"; + $sql = "insert into test_table (`id`, `name`) values ( :__dynamicValueSet )"; file_put_contents($filePath, $sql); $query = new SqlQuery($filePath, $this->driverSingleton()); $data = [ @@ -366,6 +366,31 @@ public function testDynamicBindingsWhereIn( self::assertSame("2020-01-01", $data["startDate"]); } + /** + * @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider() + */ + public function testDynamicBindingsWhereInStrings( + string $queryName, + string $queryCollectionPath, + string $filePath + ) { + $sql = "select `id`, `name` from `test_table` where `createdAt` > :startDate and `name` in ( :__dynamicIn ) limit 10"; + file_put_contents($filePath, $sql); + $query = new SqlQuery($filePath, $this->driverSingleton()); + $data = [ + "startDate" => "2020-01-01", + "__dynamicIn" => ["one", "two", "three's the last"], + ]; + $originalData = $data; + $injectedSql = $query->injectDynamicBindings($sql, $data); + + self::assertStringNotContainsString("dynamicIn", $injectedSql); + + self::assertStringContainsString("where `createdAt` > :startDate and `name` in ( 'one', 'two', 'three\'s the last' ) limit 10", $injectedSql); + self::assertArrayNotHasKey("__dynamicIn", $data); + self::assertSame("2020-01-01", $data["startDate"]); + } + /** * @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider() */ From 094856deae168d832208d98e5037f5710cb055ef Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Thu, 17 Aug 2023 21:59:09 +0100 Subject: [PATCH 6/8] tweak: remove unused variable --- test/phpunit/Query/SqlQueryTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/test/phpunit/Query/SqlQueryTest.php b/test/phpunit/Query/SqlQueryTest.php index 917f658..38ab2c4 100644 --- a/test/phpunit/Query/SqlQueryTest.php +++ b/test/phpunit/Query/SqlQueryTest.php @@ -381,7 +381,6 @@ public function testDynamicBindingsWhereInStrings( "startDate" => "2020-01-01", "__dynamicIn" => ["one", "two", "three's the last"], ]; - $originalData = $data; $injectedSql = $query->injectDynamicBindings($sql, $data); self::assertStringNotContainsString("dynamicIn", $injectedSql); From 180df869a1b4c5ee65f0776556d0b1664956e61e Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Thu, 17 Aug 2023 22:11:35 +0100 Subject: [PATCH 7/8] tweak: double single quote for proper escaping --- src/Query/SqlQuery.php | 2 +- test/phpunit/Query/SqlQueryTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index ea4364f..1914ca2 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -182,7 +182,7 @@ private function injectDynamicIn(string $sql, array &$data):string { foreach($data["__dynamicIn"] as $i => $value) { if(is_string($value)) { - $value = str_replace("'", "\'", $value); + $value = str_replace("'", "''", $value); $data["__dynamicIn"][$i] = "'$value'"; } } diff --git a/test/phpunit/Query/SqlQueryTest.php b/test/phpunit/Query/SqlQueryTest.php index 38ab2c4..4054f95 100644 --- a/test/phpunit/Query/SqlQueryTest.php +++ b/test/phpunit/Query/SqlQueryTest.php @@ -385,7 +385,7 @@ public function testDynamicBindingsWhereInStrings( self::assertStringNotContainsString("dynamicIn", $injectedSql); - self::assertStringContainsString("where `createdAt` > :startDate and `name` in ( 'one', 'two', 'three\'s the last' ) limit 10", $injectedSql); + self::assertStringContainsString("where `createdAt` > :startDate and `name` in ( 'one', 'two', 'three''s the last' ) limit 10", $injectedSql); self::assertArrayNotHasKey("__dynamicIn", $data); self::assertSame("2020-01-01", $data["startDate"]); } From 4e48d3d390d0a54be969636535dfaa1e1def82b5 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 18 Aug 2023 12:21:10 +0100 Subject: [PATCH 8/8] feature: dynamic or --- src/Query/SqlQuery.php | 38 ++++++++++++++++++++++++++++- test/phpunit/Query/SqlQueryTest.php | 27 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index 1914ca2..6bcf6c8 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -125,7 +125,9 @@ public function injectSpecialBindings( /** @param array> $data */ public function injectDynamicBindings(string $sql, array &$data):string { $sql = $this->injectDynamicBindingsValueSet($sql, $data); - return $this->injectDynamicIn($sql, $data); + $sql = $this->injectDynamicIn($sql, $data); + $sql = $this->injectDynamicOr($sql, $data); + return trim($sql); } /** @param array> $data */ @@ -192,6 +194,40 @@ private function injectDynamicIn(string $sql, array &$data):string { return str_replace($matches[0], "( $replacementString )", $sql); } + private function injectDynamicOr(string $sql, array &$data):string { + $pattern = '/:__dynamicOr/'; + if(!preg_match($pattern, $sql, $matches)) { + return $sql; + } + if(!isset($data["__dynamicOr"])) { + return $sql; + } + + $replacementString = ""; + foreach($data["__dynamicOr"] as $i => $kvp) { + $conditionString = ""; + foreach($kvp as $key => $value) { + if(is_string($value)) { + $value = str_replace("'", "''", $value); + $value = "'$value'"; + } + + if($conditionString) { + $conditionString .= " and "; + } + $conditionString .= "`$key` = $value"; + } + + if($replacementString) { + $replacementString .= " or\n"; + } + $replacementString .= "\t($conditionString)"; + } + + $replacementString = "\n(\n$replacementString\n)\n"; + return str_replace($matches[0], $replacementString, $sql); + } + /** * @param array|array $bindings * @return array|array diff --git a/test/phpunit/Query/SqlQueryTest.php b/test/phpunit/Query/SqlQueryTest.php index 4054f95..b9fdcb8 100644 --- a/test/phpunit/Query/SqlQueryTest.php +++ b/test/phpunit/Query/SqlQueryTest.php @@ -390,6 +390,33 @@ public function testDynamicBindingsWhereInStrings( self::assertSame("2020-01-01", $data["startDate"]); } + /** + * @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider() + */ + public function testDynamicBindingsOr( + string $queryName, + string $queryCollectionPath, + string $filePath, + ) { + $sql = "select `id`, `customerId`, `productId` from `Purchases` where :__dynamicOr limit 10"; + file_put_contents($filePath, $sql); + $query = new SqlQuery($filePath, $this->driverSingleton()); + $data = [ + "__dynamicOr" => [ + ["customerId" => "cust_105", "productId" => 001], + ["customerId" => "cust_450", "productId" => 941], + ["customerId" => "cust_450", "productId" => 433], + ] + ]; + $injectedSql = $query->injectDynamicBindings($sql, $data); + + self::assertStringNotContainsString("dynamicOr", $injectedSql); + + $injectedSql = str_replace(["\t", "\n"], " ", $injectedSql); + $injectedSql = str_replace(" ", " ", $injectedSql); + self::assertStringContainsString("where ( (`customerId` = 'cust_105' and `productId` = 1) or (`customerId` = 'cust_450' and `productId` = 941) or (`customerId` = 'cust_450' and `productId` = 433) ) limit 10", $injectedSql); + } + /** * @dataProvider \Gt\Database\Test\Helper\Helper::queryPathNotExistsProvider() */