diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b4185b3..7866b60 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -8,8 +8,8 @@ jobs: strategy: max-parallel: 15 matrix: - operating-system: [ubuntu-latest, windows-latest, macOS-latest] - php-versions: ['7.2', '7.3'] + operating-system: [ubuntu-latest] + php-versions: ['7.2', '7.3', '7.4'] name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} steps: - name: Checkout @@ -18,9 +18,10 @@ jobs: uses: shivammathur/setup-php@master with: php-version: ${{ matrix.php-versions }} - extension-csv: ast, mbstring, pdo - ini-values-csv: "date.timezone=UTC" + extensions: ast, mbstring, pdo + ini-values: "date.timezone=UTC" coverage: PCOV + pecl: true - name: Check PHP Version run: php -v - name: Check PHP Extensions diff --git a/composer.json b/composer.json index 806ac19..e4438a9 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "name": "Alexander Barker", "email": "alex@1stleg.com", "homepage": "https://github.com/kwhat/", - "role": "Developer" + "role": "Collaborator" } ], "support": { diff --git a/src/AbstractStatement.php b/src/AbstractStatement.php index be80868..b2a60a7 100644 --- a/src/AbstractStatement.php +++ b/src/AbstractStatement.php @@ -35,9 +35,9 @@ public function execute() try { $success = $stmt->execute($this->getValues()); if (!$success) { - $info = $stmt->errorInfo(); + list($state, $code, $message) = $stmt->errorInfo(); - throw new DatabaseException($info[2], $info[0]); + throw new DatabaseException($message, $state); } } catch (PDOException $e) { throw new DatabaseException($e->getMessage(), $e->getCode(), $e); diff --git a/src/Clause/Conditional.php b/src/Clause/Conditional.php index 93aa2dd..6902e21 100644 --- a/src/Clause/Conditional.php +++ b/src/Clause/Conditional.php @@ -43,38 +43,70 @@ public function getValues(): array $values = [$values]; } + $count = count($values); + for ($i = 0; $i < $count; $i++) { + if ($values[$i] instanceof QueryInterface) { + $value = $values[$i]->getValues(); + array_splice($values, $i, 1, $value); + $i += count($value); + } + } + return $values; } /** - * @throws DatabaseException + * @param mixed $value * * @return string + * @throws DatabaseException + */ + protected function getPlaceholder($value): string + { + $placeholder = '?'; + if ($value instanceof QueryInterface) { + $placeholder = "{$value}"; + } + + return $placeholder; + } + + /** + * @return string + * @throws DatabaseException */ public function __toString(): string { - $sql = "{$this->column} {$this->operator}"; + $sql = "{$this->column} {$this->operator} "; switch ($this->operator) { case 'BETWEEN': case 'NOT BETWEEN': - if (count($this->getValues()) != 2) { + if (count($this->value) != 2) { throw new DatabaseException('Conditional operator "BETWEEN" requires two arguments'); } - $sql .= ' (? AND ?)'; + $sql .= "({$this->getPlaceholder($this->value[0])} AND {$this->getPlaceholder($this->value[0])})"; break; case 'IN': case 'NOT IN': - if (count($this->getValues()) < 1) { + if (count($this->value) < 1) { throw new DatabaseException('Conditional operator "IN" requires at least one argument'); } - $sql .= ' (' . substr(str_repeat('?, ', count($this->getValues())), 0, -2) . ')'; + $placeholders = ''; + foreach ($this->value as $value) { + if (!empty($placeholders)) { + $placeholders .= ', '; + } + + $placeholders .= $this->getPlaceholder($value); + } + $sql .= "({$placeholders})"; break; default: - $sql .= ' ?'; + $sql .= $this->getPlaceholder($this->value); } return $sql; diff --git a/src/Clause/Grouping.php b/src/Clause/Grouping.php index 2d7bd8b..a73547a 100644 --- a/src/Clause/Grouping.php +++ b/src/Clause/Grouping.php @@ -14,11 +14,13 @@ class Grouping extends Conditional /** * @param string $rule + * @param Conditional $clause * @param Conditional ...$clauses */ - public function __construct(string $rule, Conditional ...$clauses) + public function __construct(string $rule, Conditional $clause, Conditional ...$clauses) { - parent::__construct('', strtoupper(trim($rule)), $clauses); + array_unshift($clauses, $clause); + parent::__construct('', $rule, $clauses); } /** @@ -41,13 +43,17 @@ public function __toString(): string { $sql = ''; foreach ($this->value as $clause) { - if ($clause instanceof self) { - $clause = "({$clause})"; + if (!empty($sql)) { + $sql .= " {$this->operator} "; } - $sql .= "{$clause} {$this->operator} "; + if ($clause instanceof self) { + $sql .= "({$clause})"; + } else { + $sql .= "{$clause}"; + } } - return preg_replace('/' . preg_quote(" $this->operator ", '/') . '$/', '', $sql); + return $sql; } } diff --git a/src/Clause/Join.php b/src/Clause/Join.php index a9f9e56..90f058b 100644 --- a/src/Clause/Join.php +++ b/src/Clause/Join.php @@ -43,9 +43,8 @@ public function getValues(): array } /** - * @throws DatabaseException - * * @return string + * @throws DatabaseException */ public function __toString(): string { diff --git a/src/Clause/Limit.php b/src/Clause/Limit.php index 4aed2cb..399fc33 100644 --- a/src/Clause/Limit.php +++ b/src/Clause/Limit.php @@ -21,7 +21,7 @@ class Limit implements QueryInterface * @param int $size * @param int|null $offset */ - public function __construct(int $size, int $offset = null) + public function __construct(int $size, ?int $offset = null) { $this->size = $size; $this->offset = $offset; @@ -32,11 +32,11 @@ public function __construct(int $size, int $offset = null) */ public function getValues(): array { - $values = []; - if (isset($this->offset)) { - $values[] = $this->offset; + if ($this->offset !== null) { + $values = [$this->offset, $this->size]; + } else { + $values = [$this->size]; } - $values[] = $this->size; return $values; } @@ -46,8 +46,8 @@ public function getValues(): array */ public function __toString(): string { - $sql = '?'; - if (isset($this->offset)) { + $sql = 'LIMIT ?'; + if ($this->offset !== null) { $sql .= ', ?'; } diff --git a/src/Clause/Method.php b/src/Clause/Method.php index c1c7f60..024b9e9 100644 --- a/src/Clause/Method.php +++ b/src/Clause/Method.php @@ -34,7 +34,9 @@ public function getValues(): array { $values = []; foreach ($this->values as $value) { - if (!$value instanceof QueryInterface) { + if ($value instanceof QueryInterface) { + $values = array_merge($values, $value->getValues()); + } else { $values[] = $value; } } @@ -49,11 +51,13 @@ public function __toString(): string { $placeholders = ''; foreach ($this->values as $value) { - if (!$value instanceof QueryInterface) { - if (!empty($placeholders)) { - $placeholders .= ', '; - } + if (!empty($placeholders)) { + $placeholders .= ', '; + } + if ($value instanceof QueryInterface) { + $placeholders .= "{$value}"; + } else { $placeholders .= '?'; } } diff --git a/src/Statement/Call.php b/src/Statement/Call.php index 1b31151..80f6e0c 100644 --- a/src/Statement/Call.php +++ b/src/Statement/Call.php @@ -55,18 +55,15 @@ public function getValues(): array } /** - * @throws DatabaseException - * * @return string + * @throws DatabaseException */ public function __toString(): string { if (!$this->method instanceof Clause\Method) { - throw new DatabaseException('No method is set for stored procedure call'); + throw new DatabaseException('No method set for call statement'); } - $sql = "CALL {$this->method}"; - - return $sql; + return "CALL {$this->method}"; } } diff --git a/src/Statement/Delete.php b/src/Statement/Delete.php index b17f77a..7ea544b 100644 --- a/src/Statement/Delete.php +++ b/src/Statement/Delete.php @@ -67,9 +67,8 @@ public function getValues(): array } /** - * @throws DatabaseException - * * @return string + * @throws DatabaseException */ public function __toString(): string { @@ -95,7 +94,7 @@ public function __toString(): string $sql .= ' ' . implode(' ', $this->join); } - if ($this->where != null) { + if ($this->where !== null) { $sql .= " WHERE {$this->where}"; } @@ -107,8 +106,8 @@ public function __toString(): string $sql = substr($sql, 0, -2); } - if ($this->limit != null) { - $sql .= " LIMIT {$this->limit}"; + if ($this->limit !== null) { + $sql .= " {$this->limit}"; } return $sql; diff --git a/src/Statement/Insert.php b/src/Statement/Insert.php index ae27e8f..8a18dd2 100644 --- a/src/Statement/Insert.php +++ b/src/Statement/Insert.php @@ -97,9 +97,8 @@ public function ignore(): self } /** - * @throws DatabaseException - * * @return string + * @throws DatabaseException */ public function __toString(): string { @@ -107,35 +106,47 @@ public function __toString(): string throw new DatabaseException('No table is set for insertion'); } - if (empty($this->columns)) { + $size = count($this->values); + if ($size < 1) { throw new DatabaseException('Missing columns for insertion'); } - if (empty($this->values) || count($this->columns) != count($this->values)) { + if (count($this->columns) > 0 && count($this->columns) != count($this->values)) { throw new DatabaseException('Missing values for insertion'); } - $placeholders = ''; - foreach ($this->values as $value) { - if (!empty($placeholders)) { - $placeholders .= ', '; + if ($this->values[0] instanceof Select) { + if (count($this->values) > 1) { + throw new DatabaseException('Ignoring additional values after select for insert statement'); } - if ($value instanceof QueryInterface) { - $placeholders .= "{$value}"; - } else { - $placeholders .= '?'; + $placeholders = " {$this->values[0]}"; + } else { + $plug = ''; + foreach ($this->values as $value) { + if (!empty($plug)) { + $plug .= ', '; + } + + if ($value instanceof QueryInterface) { + $plug .= "{$value}"; + } else { + $plug .= '?'; + } } - } - $columns = implode(', ', $this->columns); + $placeholders = " VALUES ({$plug})"; + } $sql = 'INSERT'; if ($this->ignore) { $sql .= ' IGNORE'; } - $sql .= " INTO {$this->table} ({$columns})"; - $sql .= " VALUES ({$placeholders})"; + $sql .= " INTO {$this->table}"; + if (!empty($this->columns)) { + $sql .= ' (' . implode(', ', $this->columns) . ')'; + } + $sql .= "{$placeholders}"; return $sql; } @@ -158,9 +169,8 @@ public function getValues(): array } /** - * @throws DatabaseException - * * @return int|string + * @throws DatabaseException */ public function execute() { diff --git a/src/Statement/Select.php b/src/Statement/Select.php index 2a69b51..e92b28a 100644 --- a/src/Statement/Select.php +++ b/src/Statement/Select.php @@ -144,15 +144,15 @@ public function getValues(): array $values = array_merge($values, $join->getValues()); } - if ($this->where !== null) { + if ($this->where != null) { $values = array_merge($values, $this->where->getValues()); } - if ($this->having !== null) { + if ($this->having != null) { $values = array_merge($values, $this->having->getValues()); } - if ($this->limit !== null) { + if ($this->limit != null) { $values = array_merge($values, $this->limit->getValues()); } @@ -187,14 +187,13 @@ protected function getColumns(): string } /** - * @throws DatabaseException - * * @return string + * @throws DatabaseException */ public function __toString(): string { if (empty($this->table)) { - throw new DatabaseException('No table is set for selection'); + throw new DatabaseException('No table set for select statement'); } $sql = 'SELECT'; @@ -226,7 +225,7 @@ public function __toString(): string $sql .= ' ' . implode(' ', $this->join); } - if ($this->where != null) { + if ($this->where !== null) { $sql .= " WHERE {$this->where}"; } @@ -234,7 +233,7 @@ public function __toString(): string $sql .= ' GROUP BY ' . implode(', ', $this->groupBy); } - if ($this->having != null) { + if ($this->having !== null) { $sql .= " HAVING {$this->having}"; } @@ -246,8 +245,8 @@ public function __toString(): string $sql = substr($sql, 0, -2); } - if ($this->limit != null) { - $sql .= " LIMIT {$this->limit}"; + if ($this->limit !== null) { + $sql .= " {$this->limit}"; } if (!empty($this->union)) { diff --git a/src/Statement/Update.php b/src/Statement/Update.php index e1ea2b5..68efe3b 100644 --- a/src/Statement/Update.php +++ b/src/Statement/Update.php @@ -108,18 +108,17 @@ protected function getColumns(): string } /** - * @throws DatabaseException - * * @return string + * @throws DatabaseException */ public function __toString(): string { if (!isset($this->table)) { - throw new DatabaseException('No table is set for update'); + throw new DatabaseException('No table set for update statement'); } if (empty($this->pairs)) { - throw new DatabaseException('Missing columns and values for update'); + throw new DatabaseException('No column / value pairs set for update statement'); } $sql = "UPDATE {$this->table}"; @@ -128,7 +127,7 @@ public function __toString(): string } $sql .= " SET {$this->getColumns()}"; - if ($this->where != null) { + if ($this->where !== null) { $sql .= " WHERE {$this->where}"; } @@ -140,17 +139,16 @@ public function __toString(): string $sql = substr($sql, 0, -2); } - if ($this->limit != null) { - $sql .= " LIMIT {$this->limit}"; + if ($this->limit !== null) { + $sql .= " {$this->limit}"; } return $sql; } /** - * @throws DatabaseException - * * @return int + * @throws DatabaseException */ public function execute() { diff --git a/tests/Clause/ConditionalTest.php b/tests/Clause/ConditionalTest.php index 49f0fd8..00dd3b7 100644 --- a/tests/Clause/ConditionalTest.php +++ b/tests/Clause/ConditionalTest.php @@ -50,6 +50,20 @@ public function testToStringWithBetweenException() $subject->__toString(); } + public function testToStringWithQuery() + { + $subject = new Clause\Conditional('col', '=', new Clause\Raw(1)); + + $this->assertEquals('col = 1', $subject->__toString()); + } + + public function testToStringWithQueryAndArgs() + { + $subject = new Clause\Conditional('col', '=', new Clause\Method('test', 1, 2)); + + $this->assertEquals('col = test(?, ?)', $subject->__toString()); + } + public function testGetValues() { $subject = new Clause\Conditional('col', '=', 'val'); @@ -57,4 +71,21 @@ public function testGetValues() $this->assertIsArray($subject->getValues()); $this->assertCount(1, $subject->getValues()); } + + public function testGetValuesWithQuery() + { + $subject = new Clause\Conditional('col', '=', new Clause\Raw(1)); + + $this->assertIsArray($subject->getValues()); + $this->assertCount(0, $subject->getValues()); + $this->assertEquals('col = 1', $subject->__toString()); + } + + public function testGetValuesWithQueryAndArgs() + { + $subject = new Clause\Conditional('col', '=', new Clause\Method('test', 1, 2)); + + $this->assertIsArray($subject->getValues()); + $this->assertCount(2, $subject->getValues()); + } } diff --git a/tests/Clause/LimitTest.php b/tests/Clause/LimitTest.php index 215a26e..bd5efa2 100644 --- a/tests/Clause/LimitTest.php +++ b/tests/Clause/LimitTest.php @@ -16,14 +16,14 @@ public function testToStringWithOffset() { $subject = new Clause\Limit(10, 25); - $this->assertEquals('?, ?', $subject->__toString()); + $this->assertEquals('LIMIT ?, ?', $subject->__toString()); } public function testToStringWithoutOffset() { $subject = new Clause\Limit(10); - $this->assertEquals('?', $subject->__toString()); + $this->assertEquals('LIMIT ?', $subject->__toString()); } public function testGetValuesWithOffset() diff --git a/tests/Clause/MethodTest.php b/tests/Clause/MethodTest.php index 7fb0204..6f278f0 100644 --- a/tests/Clause/MethodTest.php +++ b/tests/Clause/MethodTest.php @@ -12,6 +12,13 @@ class MethodTest extends TestCase { + public function testToString() + { + $subject = new Clause\Method('test'); + + $this->assertEquals('test()', $subject->__toString()); + } + public function testToStringWithArgs() { $subject = new Clause\Method('test', 1, 2); @@ -19,11 +26,26 @@ public function testToStringWithArgs() $this->assertEquals('test(?, ?)', $subject->__toString()); } - public function testToStringWithoutArgs() + public function testToStringWithQuery() + { + $subject = new Clause\Method('test', new Clause\Raw(1)); + + $this->assertEquals('test(1)', $subject->__toString()); + } + + public function testToStringWithQueryAndArgs() + { + $subject = new Clause\Method('test', new Clause\Method('next', 1, 2)); + + $this->assertEquals('test(next(?, ?))', $subject->__toString()); + } + + public function testGetValues() { $subject = new Clause\Method('test'); - $this->assertEquals('test()', $subject->__toString()); + $this->assertIsArray($subject->getValues()); + $this->assertEmpty($subject->getValues()); } public function testGetValuesWithArgs() @@ -41,4 +63,12 @@ public function testGetValuesWithoutArgs() $this->assertIsArray($subject->getValues()); $this->assertEmpty($subject->getValues()); } + + public function testGetValuesWithQueryAndArgs() + { + $subject = new Clause\Method('test', new Clause\Method('next', 1, 2)); + + $this->assertIsArray($subject->getValues()); + $this->assertCount(2, $subject->getValues()); + } } diff --git a/tests/Statement/InsertTest.php b/tests/Statement/InsertTest.php index 356ac88..18ce8a0 100644 --- a/tests/Statement/InsertTest.php +++ b/tests/Statement/InsertTest.php @@ -45,7 +45,7 @@ public function testToString() ->columns('one', 'two') ->values(1, 2); - $this->assertStringStartsWith('INSERT INTO test', $this->subject->__toString()); + $this->assertEquals('INSERT INTO test (one, two) VALUES (?, ?)', $this->subject->__toString()); } public function testToStringWithoutTable() @@ -58,14 +58,34 @@ public function testToStringWithoutTable() ->execute(); } - public function testToStringWithoutColumns() + public function testToStringWithColumns() { + $this->subject + ->into('test') + ->columns('col1', 'col2') + ->values(1, 2); + + $this->assertEquals('INSERT INTO test (col1, col2) VALUES (?, ?)', $this->subject->__toString()); + } + + public function testToStringWithColumnMismatch() + { + $this->subject + ->into('test') + ->columns('col2') + ->values(1, 2); + $this->expectException(DatabaseException::class); + $this->subject->__toString(); + } + public function testToStringWithoutColumns() + { $this->subject ->into('test') - ->values(1, 2) - ->execute(); + ->values(1, 2); + + $this->assertEquals('INSERT INTO test VALUES (?, ?)', $this->subject->__toString()); } public function testToStringWithoutValues() @@ -78,6 +98,32 @@ public function testToStringWithoutValues() ->execute(); } + public function testToStringWithSelect() + { + $select = new Statement\Select($this->createMock(PDO::class)); + $select->from('table'); + + $this->subject + ->into('test') + ->values($select) + ->execute(); + + $this->assertEquals('INSERT INTO test SELECT * FROM table', $this->subject->__toString()); + } + + public function testToStringWithSelectAndArgs() + { + $this->expectException(DatabaseException::class); + + $select = new Statement\Select($this->createMock(PDO::class)); + $select->from('table'); + + $this->subject + ->into('test') + ->values($select, 2) + ->execute(); + } + public function testToStringWithIgnore() { $this->subject