Skip to content

Commit

Permalink
Dynamic sets (#356)
Browse files Browse the repository at this point in the history
* feature: implement dynamic value set
closes #355

* tweak: dynamic value set syntax

* feature: implement dynamic in
closes #347

* tidy: dynamic injection

* wip: automatically wrap strings in single quotes

* tweak: remove unused variable

* tweak: double single quote for proper escaping

* feature: dynamic or
  • Loading branch information
g105b authored Sep 13, 2023
1 parent f9d3562 commit ceac0a6
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 2 deletions.
115 changes: 113 additions & 2 deletions src/Query/SqlQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ class SqlQuery extends Query {
];

/** @param array<string, mixed>|array<mixed> $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<string, mixed>|array<mixed> $bindings */
Expand Down Expand Up @@ -117,6 +122,112 @@ public function injectSpecialBindings(
return $sql;
}

/** @param array<string, string|array<string, string>> $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<string, string|array<string, string>> $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<string, string|array<string, string>> $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<string, mixed>|array<mixed> $bindings
* @return array<string, string>|array<string>
Expand Down
116 changes: 116 additions & 0 deletions test/phpunit/Query/SqlQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
*/
Expand Down

0 comments on commit ceac0a6

Please sign in to comment.