diff --git a/src/Count.php b/src/Count.php
new file mode 100644
index 0000000..ca945b2
--- /dev/null
+++ b/src/Count.php
@@ -0,0 +1,43 @@
+prepare($query);
+ foreach ($parameters as $key => $value) {
+ if ($debug) echo "
Binding $value to $key\n
";
+ $stmt->bindValue($key, $value);
+ }
+ if ($debug) echo 'SQL: ' . $stmt->queryString . '
';
+ $stmt->execute();
+ return $stmt;
+ }
+
+ /**
+ * Execute a query that returns a list of results (lazy)
+ *
+ * @template TDoc The domain type of the document to retrieve
+ * @param string $query The query to be executed
+ * @param array $parameters Parameters to use in executing the query
+ * @param Mapper $mapper Mapper to deserialize the result
+ * @return DocumentList The items matching the query
+ * @throws DocumentException If any is encountered
+ */
+ public static function list(string $query, array $parameters, Mapper $mapper): DocumentList
+ {
+ return DocumentList::create($query, $parameters, $mapper);
+ }
+
+ /**
+ * Execute a query that returns an array of results (eager)
+ *
+ * @template TDoc The domain type of the document to retrieve
+ * @param string $query The query to be executed
+ * @param array $parameters Parameters to use in executing the query
+ * @param Mapper $mapper Mapper to deserialize the result
+ * @return TDoc[] The items matching the query
+ * @throws DocumentException If any is encountered
+ */
+ public static function array(string $query, array $parameters, Mapper $mapper): array
+ {
+ return iterator_to_array(self::list($query, $parameters, $mapper)->items());
+ }
+
+ /**
+ * Execute a query that returns one or no results (returns false if not found)
+ *
+ * @template TDoc The domain type of the document to retrieve
+ * @param string $query The query to be executed (will have "LIMIT 1" appended)
+ * @param array $parameters Parameters to use in executing the query
+ * @param Mapper $mapper Mapper to deserialize the result
+ * @return false|TDoc The item if it is found, false if not
+ * @throws DocumentException If any is encountered
+ */
+ public static function single(string $query, array $parameters, Mapper $mapper): mixed
+ {
+ return empty($results = self::array("$query LIMIT 1", $parameters, $mapper)) ? false : $results[0];
+ }
+
+ /**
+ * Execute a query that does not return a value
+ *
+ * @param string $query The query to execute
+ * @param array $parameters Parameters to use in executing the query
+ * @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
+ * @throws DocumentException If any is encountered
+ */
+ public static function nonQuery(string $query, array $parameters, ?PDO $pdo = null): void
+ {
+ $stmt = self::runQuery($query, $parameters, $pdo ?? Configuration::dbConn());
+ if ($stmt->errorCode()) throw new DocumentException('Error executing command: ' . $stmt->errorCode());
+ }
+
+ /**
+ * Execute a query that returns a scalar value
+ *
+ * @template T The scalar type to return
+ * @param string $query The query to retrieve the value
+ * @param array $parameters Parameters to use in executing the query
+ * @param Mapper $mapper The mapper to obtain the result
+ * @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
+ * @return mixed|false|T The scalar value if found, false if not
+ * @throws DocumentException If any is encountered
+ */
+ public static function scalar(string $query, array $parameters, Mapper $mapper, ?PDO $pdo = null): mixed
+ {
+ $stmt = self::runQuery($query, $parameters, $pdo ?? Configuration::dbConn());
+ if ($stmt->errorCode()) {
+ throw new DocumentException('Error retrieving scalar value: ' . $stmt->errorCode());
+ }
+ if ($stmt->rowCount() > 0) {
+ $first = $stmt->fetch(PDO::FETCH_NUM);
+ return $first ? $mapper->map($first) : false;
+ }
+ return false;
+ }
+}
diff --git a/src/Definition.php b/src/Definition.php
new file mode 100644
index 0000000..85b0169
--- /dev/null
+++ b/src/Definition.php
@@ -0,0 +1,38 @@
+ The query results as a lazily-iterated generator
+ * @param PDO $pdo The database connection against which the query was opened
+ * @param PDOStatement $result The result of the query
+ * @param Mapper $mapper The mapper to deserialize JSON
*/
- public function items(): Generator;
+ private function __construct(private PDO $pdo, private PDOStatement $result, private Mapper $mapper) { }
/**
* Construct a new document list
@@ -25,12 +30,28 @@ interface DocumentList
* @param string $query The query to run to retrieve results
* @param array $parameters An associative array of parameters for the query
* @param Mapper $mapper A mapper to deserialize JSON documents
- * @return static The `DocumentList`-implementing instance
+ * @return static The document list instance
+ * @throws DocumentException If any is encountered
*/
- public static function create(string $query, array $parameters, Mapper $mapper): static;
+ public static function create(string $query, array $parameters, Mapper $mapper): static
+ {
+ $pdo = Configuration::dbConn();
+ $stmt = Custom::runQuery($query, $parameters, $pdo);
+ if ($stmt->errorCode()) {
+ throw new DocumentException('Error retrieving data: ' . $stmt->errorCode());
+ }
+ return new static($pdo, $stmt, $mapper);
+ }
/**
- * Clean up database connection resources
+ * The items from the query result
+ *
+ * @return Generator The items from the document list
*/
- public function __destruct();
+ public function items(): Generator
+ {
+ if ($this->result) {
+ while ($row = $this->result->fetch(PDO::FETCH_ASSOC)) yield $this->mapper->map($row);
+ }
+ }
}
diff --git a/src/Mapper/CountMapper.php b/src/Mapper/CountMapper.php
new file mode 100644
index 0000000..a2922a7
--- /dev/null
+++ b/src/Mapper/CountMapper.php
@@ -0,0 +1,17 @@
+ (bool)$result[0],
+ Mode::SQLite => (int)$result[0] > 0,
+ default => throw new DocumentException('Database mode not set; cannot map existence result'),
+ };
+ }
+}
diff --git a/src/Parameters.php b/src/Parameters.php
new file mode 100644
index 0000000..592b135
--- /dev/null
+++ b/src/Parameters.php
@@ -0,0 +1,81 @@
+ is_string($key) ? $key : "$key"];
+ }
+
+ /**
+ * Create a parameter with a JSON value
+ *
+ * @param string $name The name of the JSON parameter
+ * @param object|array $document The value that should be passed as a JSON string
+ * @return array An associative array with the named parameter/value pair
+ */
+ public static function json(string $name, object|array $document): array
+ {
+ return [$name => json_encode($document, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)];
+ }
+
+ /**
+ * Fill in parameter names for any fields missing one
+ *
+ * @param array|Field[] $fields The fields for the query
+ * @return array|Field[] The fields, all with non-blank parameter names
+ */
+ public static function nameFields(array $fields): array
+ {
+ for ($idx = 0; $idx < sizeof($fields); $idx++) {
+ if ($fields[$idx]->paramName == '') $fields[$idx]->paramName = "@field$idx";
+ }
+ return $fields;
+ }
+
+ /**
+ * Add field parameters to the given set of parameters
+ *
+ * @param array|Field[] $fields The fields being compared in the query
+ * @param array $parameters An associative array of parameters to which the fields should be added
+ * @return array An associative array of parameter names and values with the fields added
+ */
+ public static function addFields(array $fields, array $parameters): array
+ {
+ return array_reduce($fields, fn($carry, $item) => $item->appendParameter($carry), $parameters);
+ }
+
+ /**
+ * Create JSON field name parameters for the given field names to the given parameter
+ *
+ * @param string $paramName The name of the parameter for the field names
+ * @param array|string[] $fieldNames The names of the fields for the parameter
+ * @return array An associative array of parameter/value pairs for the field names
+ * @throws DocumentException If the database mode has not been set
+ */
+ public static function fieldNames(string $paramName, array $fieldNames): array
+ {
+ switch (Configuration::$mode) {
+ case Mode::PgSQL:
+ return [$paramName => "ARRAY['" . implode("','", $fieldNames) . "']"];
+ case Mode::SQLite:
+ $it = [];
+ $idx = 0;
+ foreach ($fieldNames as $field) $it[$paramName . $idx++] = $field;
+ return $it;
+ default:
+ throw new DocumentException('Database mode not set; cannot generate field name parameters');
+ }
+ }
+}
diff --git a/tests/unit/ConfigurationTest.php b/tests/unit/ConfigurationTest.php
index 2efbf6d..6ff0c09 100644
--- a/tests/unit/ConfigurationTest.php
+++ b/tests/unit/ConfigurationTest.php
@@ -12,11 +12,13 @@ use PHPUnit\Framework\TestCase;
*/
class ConfigurationTest extends TestCase
{
+ #[TestDox('ID field default succeeds')]
public function testIdFieldDefaultSucceeds(): void
{
$this->assertEquals('id', Configuration::$idField, 'Default ID field should be "id"');
}
+ #[TestDox('ID field change succeeds')]
public function testIdFieldChangeSucceeds()
{
try {
diff --git a/tests/unit/Mapper/CountMapperTest.php b/tests/unit/Mapper/CountMapperTest.php
new file mode 100644
index 0000000..130763a
--- /dev/null
+++ b/tests/unit/Mapper/CountMapperTest.php
@@ -0,0 +1,17 @@
+assertEquals(5, (new CountMapper())->map([5, 8, 10]), 'Count not correct');
+ }
+}
diff --git a/tests/unit/Mapper/ExistsMapperTest.php b/tests/unit/Mapper/ExistsMapperTest.php
new file mode 100644
index 0000000..ce68908
--- /dev/null
+++ b/tests/unit/Mapper/ExistsMapperTest.php
@@ -0,0 +1,45 @@
+assertFalse((new ExistsMapper())->map([false, 'nope']), 'Result should have been false');
+ } finally {
+ Configuration::$mode = null;
+ }
+ }
+
+ #[TestDox('Map succeeds for SQLite')]
+ public function testMapSucceedsForSQLite(): void
+ {
+ try {
+ Configuration::$mode = Mode::SQLite;
+ $this->assertTrue((new ExistsMapper())->map([1, 'yep']), 'Result should have been true');
+ } finally {
+ Configuration::$mode = null;
+ }
+ }
+
+ public function testMapFailsWhenModeNotSet(): void
+ {
+ $this->expectException(DocumentException::class);
+ Configuration::$mode = null;
+ (new ExistsMapper())->map(['0']);
+ }
+}
diff --git a/tests/unit/ParametersTest.php b/tests/unit/ParametersTest.php
new file mode 100644
index 0000000..1a0627c
--- /dev/null
+++ b/tests/unit/ParametersTest.php
@@ -0,0 +1,83 @@
+assertEquals(['@id' => 'key'], Parameters::id('key'), 'ID parameter not constructed correctly');
+ }
+
+ #[TestDox('ID succeeds with non string')]
+ public function testIdSucceedsWithNonString(): void
+ {
+ $this->assertEquals(['@id' => '7'], Parameters::id(7), 'ID parameter not constructed correctly');
+ }
+
+ public function testJsonSucceeds(): void
+ {
+ $this->assertEquals(['@it' => '{"id":18,"url":"https://www.unittest.com"}'],
+ Parameters::json('@it', ['id' => 18, 'url' => 'https://www.unittest.com']),
+ 'JSON parameter not constructed correctly');
+ }
+
+ public function testNameFieldsSucceeds(): void
+ {
+ $named = Parameters::nameFields([Field::EQ('it', 17), Field::EQ('also', 22, '@also'), Field::EQ('other', 24)]);
+ $this->assertCount(3, $named, 'There should be 3 parameters in the array');
+ $this->assertEquals('@field0', $named[0]->paramName, 'Parameter 1 not named correctly');
+ $this->assertEquals('@also', $named[1]->paramName, 'Parameter 2 not named correctly');
+ $this->assertEquals('@field2', $named[2]->paramName, 'Parameter 3 not named correctly');
+ }
+
+ public function testAddFieldsSucceeds(): void
+ {
+ $this->assertEquals(['@a' => 1, '@b' => 'two', '@z' => 18],
+ Parameters::addFields([Field::EQ('b', 'two', '@b'), Field::EQ('z', 18, '@z')], ['@a' => 1]),
+ 'Field parameters not added correctly');
+ }
+
+ #[TestDox('Field names succeeds for PostgreSQL')]
+ public function testFieldNamesSucceedsForPostgreSQL(): void
+ {
+ try {
+ Configuration::$mode = Mode::PgSQL;
+ $this->assertEquals(['@names' => "ARRAY['one','two','seven']"],
+ Parameters::fieldNames('@names', ['one', 'two', 'seven']), 'Field name parameters not correct');
+ } finally {
+ Configuration::$mode = null;
+ }
+ }
+
+ #[TestDox('Field names succeeds for SQLite')]
+ public function testFieldNamesSucceedsForSQLite(): void
+ {
+ try {
+ Configuration::$mode = Mode::SQLite;
+ $this->assertEquals(['@it0' => 'test', '@it1' => 'unit', '@it2' => 'wow'],
+ Parameters::fieldNames('@it', ['test', 'unit', 'wow']), 'Field name parameters not correct');
+ } finally {
+ Configuration::$mode = null;
+ }
+ }
+
+ public function testFieldNamesFailsWhenModeNotSet(): void
+ {
+ $this->expectException(DocumentException::class);
+ Configuration::$mode = null;
+ Parameters::fieldNames('', []);
+ }
+}