diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index e07303f..6bcf6c8 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,6 +122,112 @@ 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); + $sql = $this->injectDynamicOr($sql, $data); + return trim($sql); + } + + /** @param array> $data */ + 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 = []; + foreach($data["__dynamicValueSet"] 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); + } + unset($data["__dynamicValueSet"]); + + $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> $data */ + 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; + } + + 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); + } + + 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 f792c95..b9fdcb8 100644 --- a/test/phpunit/Query/SqlQueryTest.php +++ b/test/phpunit/Query/SqlQueryTest.php @@ -301,6 +301,122 @@ 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 = [ + "__dynamicValueSet" => [ + ["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["__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() + */ + 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"], + ]; + $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() + */ + 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() */