diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index c5f21759..d6274b18 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -21,7 +21,6 @@
./test/unit/Adapter/AdapterServiceFactoryTest.php
./test/unit/Adapter/Driver/Pdo/ConnectionIntegrationTest.php
./test/unit/Adapter/Driver/Pdo/StatementIntegrationTest.php
- ./test/unit/Adapter/AdapterAwareTraitTest.php
./test/integration
diff --git a/src/Adapter/Driver/Pdo/AbstractPdo.php b/src/Adapter/Driver/Pdo/AbstractPdo.php
index 1984c786..048bf530 100644
--- a/src/Adapter/Driver/Pdo/AbstractPdo.php
+++ b/src/Adapter/Driver/Pdo/AbstractPdo.php
@@ -73,9 +73,11 @@ public function getProfiler(): ?ProfilerInterface
public function checkEnvironment(): bool
{
if (! extension_loaded('PDO')) {
+ // @codeCoverageIgnoreStart
throw new Exception\RuntimeException(
'The PDO extension is required for this adapter but the extension is not loaded'
);
+ // @codeCoverageIgnoreEnd
}
return true;
}
diff --git a/src/Adapter/Profiler/Profiler.php b/src/Adapter/Profiler/Profiler.php
index dae0b7e4..10a5a72f 100644
--- a/src/Adapter/Profiler/Profiler.php
+++ b/src/Adapter/Profiler/Profiler.php
@@ -5,11 +5,9 @@
namespace PhpDb\Adapter\Profiler;
use PhpDb\Adapter\Exception;
-use PhpDb\Adapter\Exception\InvalidArgumentException;
use PhpDb\Adapter\StatementContainerInterface;
use function end;
-use function is_string;
use function microtime;
class Profiler implements ProfilerInterface
@@ -20,10 +18,7 @@ class Profiler implements ProfilerInterface
/** @var int */
protected $currentIndex = 0;
- /**
- * @throws InvalidArgumentException
- * @return $this Provides a fluent interface
- */
+ /** @return $this Provides a fluent interface */
public function profilerStart(string|StatementContainerInterface $target): ProfilerInterface
{
$profileInformation = [
@@ -39,12 +34,8 @@ public function profilerStart(string|StatementContainerInterface $target): Profi
if ($container !== null) {
$profileInformation['parameters'] = clone $container;
}
- } elseif (is_string($target)) {
- $profileInformation['sql'] = $target;
} else {
- throw new Exception\InvalidArgumentException(
- __FUNCTION__ . ' takes either a StatementContainer or a string'
- );
+ $profileInformation['sql'] = $target;
}
$this->profiles[$this->currentIndex] = $profileInformation;
diff --git a/src/ResultSet/AbstractResultSet.php b/src/ResultSet/AbstractResultSet.php
index d9c4d38b..b909f4c1 100644
--- a/src/ResultSet/AbstractResultSet.php
+++ b/src/ResultSet/AbstractResultSet.php
@@ -20,7 +20,6 @@
use function gettype;
use function is_array;
use function is_object;
-use function key;
use function method_exists;
use function reset;
@@ -80,12 +79,9 @@ public function initialize(iterable $dataSource): ResultSetInterface
} elseif ($dataSource instanceof IteratorAggregate) {
/** @phpstan-ignore assign.propertyType */
$this->dataSource = $dataSource->getIterator();
- } elseif ($dataSource instanceof Iterator) {
- $this->dataSource = $dataSource;
} else {
- throw new InvalidArgumentException(
- 'DataSource provided is not an array, nor does it implement Iterator or IteratorAggregate'
- );
+ /** @phpstan-ignore assign.propertyType */
+ $this->dataSource = $dataSource;
}
return $this;
@@ -214,12 +210,7 @@ public function valid(): bool
return true;
}
- if ($this->dataSource instanceof Iterator) {
- return $this->dataSource->valid();
- } else {
- $key = key($this->dataSource);
- return $key !== null;
- }
+ return $this->dataSource->valid();
}
/**
@@ -229,11 +220,7 @@ public function valid(): bool
public function rewind(): void
{
if (! is_array($this->buffer)) {
- if ($this->dataSource instanceof Iterator) {
- $this->dataSource->rewind();
- } else {
- reset($this->dataSource);
- }
+ $this->dataSource->rewind();
}
$this->position = 0;
diff --git a/src/Sql/AbstractSql.php b/src/Sql/AbstractSql.php
index d7170b30..eb5350a6 100644
--- a/src/Sql/AbstractSql.php
+++ b/src/Sql/AbstractSql.php
@@ -21,11 +21,8 @@
use function count;
use function current;
use function get_object_vars;
-use function gettype;
use function implode;
use function is_array;
-use function is_callable;
-use function is_object;
use function is_string;
use function key;
use function rtrim;
@@ -151,14 +148,6 @@ protected function processExpression(
: $platform->quoteValue((string) $argument->getValue()),
$argument instanceof Identifier => $platform->quoteIdentifierInFragment($argument->getValue()),
$argument instanceof Literal => $argument->getValue(),
- $argument instanceof Values => $this->processValuesArgument(
- $argument,
- $expressionParamIndex,
- $namedParameterPrefix,
- $platform,
- $driver,
- $parameterContainer
- ),
$argument instanceof Identifiers => $this->processIdentifiersArgument($argument, $platform),
$argument instanceof SelectArgument => $this->processExpressionOrSelect(
$argument,
@@ -234,36 +223,6 @@ protected function processExpressionOrSelect(
};
}
- protected function processValuesArgument(
- ArgumentInterface $argument,
- int &$expressionParamIndex,
- string $namedParameterPrefix,
- PlatformInterface $platform,
- ?DriverInterface $driver = null,
- ?ParameterContainer $parameterContainer = null
- ): string {
- $values = $argument->getValue();
- $processedValues = [];
-
- if ($parameterContainer instanceof ParameterContainer) {
- foreach ($values as $value) {
- $processedValues[] = $this->processExpressionParameterName(
- $value,
- $namedParameterPrefix,
- $expressionParamIndex,
- $driver,
- $parameterContainer
- );
- }
- } else {
- foreach ($values as $value) {
- $processedValues[] = $platform->quoteValue((string) $value);
- }
- }
-
- return implode(', ', $processedValues);
- }
-
protected function processIdentifiersArgument(
ArgumentInterface $argument,
PlatformInterface $platform
@@ -419,13 +378,8 @@ protected function processJoin(
: '') . $platform->quoteIdentifier($joinName[0]);
} elseif ($joinName instanceof Select) {
$joinName = '(' . $this->processSubSelect($joinName, $platform, $driver, $parameterContainer) . ')';
- } elseif (is_string($joinName) || (is_object($joinName) && is_callable([$joinName, '__toString']))) {
- $joinName = $platform->quoteIdentifier($joinName);
} else {
- throw new Exception\InvalidArgumentException(sprintf(
- 'Join name expected to be Expression|TableIdentifier|Select|string, "%s" given',
- gettype($joinName)
- ));
+ $joinName = $platform->quoteIdentifier($joinName);
}
$joinSpecArgArray[$j] = [
@@ -511,9 +465,6 @@ protected function resolveTable(
return $table;
}
- /**
- * Copy variables from the subject into the local properties
- */
protected function localizeVariables(): void
{
if (! $this instanceof PlatformDecoratorInterface) {
diff --git a/test/unit/Adapter/AdapterAwareTraitTest.php b/test/unit/Adapter/AdapterAwareTraitTest.php
index b133fcfc..87e8ac74 100644
--- a/test/unit/Adapter/AdapterAwareTraitTest.php
+++ b/test/unit/Adapter/AdapterAwareTraitTest.php
@@ -6,24 +6,21 @@
use PhpDb\Adapter\Adapter;
use PhpDb\Adapter\AdapterAwareTrait;
-use PhpDb\Adapter\AdapterInterface;
use PhpDb\Adapter\Driver\DriverInterface;
use PhpDb\Adapter\Platform\PlatformInterface;
+use PhpDbTest\Adapter\TestAsset\ConcreteAdapterAwareObject;
+use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
+#[CoversMethod(AdapterAwareTrait::class, 'setDbAdapter')]
+#[Group('unit')]
class AdapterAwareTraitTest extends TestCase
{
public function testSetDbAdapter(): void
{
- $object = new class {
- use AdapterAwareTrait;
-
- public function getAdapter(): ?AdapterInterface
- {
- return $this->adapter ?? null;
- }
- };
+ $object = new ConcreteAdapterAwareObject();
self::assertNull($object->getAdapter());
@@ -39,9 +36,7 @@ public function getAdapter(): ?AdapterInterface
public function testSetDbAdapterSetsProperty(): void
{
- $object = new class {
- use AdapterAwareTrait;
- };
+ $object = new ConcreteAdapterAwareObject();
$driver = $this->createMock(DriverInterface::class);
$platform = $this->createMock(PlatformInterface::class);
diff --git a/test/unit/Adapter/AdapterTest.php b/test/unit/Adapter/AdapterTest.php
index 5cb21b23..5640b182 100644
--- a/test/unit/Adapter/AdapterTest.php
+++ b/test/unit/Adapter/AdapterTest.php
@@ -11,6 +11,8 @@
use PhpDb\Adapter\Driver\DriverInterface;
use PhpDb\Adapter\Driver\ResultInterface;
use PhpDb\Adapter\Driver\StatementInterface;
+use PhpDb\Adapter\Exception\InvalidArgumentException;
+use PhpDb\Adapter\Exception\VunerablePlatformQuoteException;
use PhpDb\Adapter\ParameterContainer;
use PhpDb\Adapter\Platform\PlatformInterface;
use PhpDb\Adapter\Profiler;
@@ -26,8 +28,6 @@
#[CoversMethod(Adapter::class, 'setProfiler')]
#[CoversMethod(Adapter::class, 'getProfiler')]
-#[CoversMethod(Adapter::class, 'createDriver')]
-#[CoversMethod(Adapter::class, 'createPlatform')]
#[CoversMethod(Adapter::class, 'getDriver')]
#[CoversMethod(Adapter::class, 'getPlatform')]
#[CoversMethod(Adapter::class, 'getQueryResultSetPrototype')]
@@ -35,6 +35,9 @@
#[CoversMethod(Adapter::class, 'query')]
#[CoversMethod(Adapter::class, 'createStatement')]
#[CoversMethod(Adapter::class, '__get')]
+#[CoversMethod(Adapter::class, '__construct')]
+#[CoversMethod(Adapter::class, 'getHelpers')]
+#[Group('unit')]
final class AdapterTest extends TestCase
{
protected DriverInterface&MockObject $mockDriver;
@@ -236,4 +239,93 @@ public function test__get(): void
/** @phpstan-ignore property.notFound, expr.resultUnused */
$this->adapter->foo;
}
+
+ public function testGetHelpersReturnsQuoteIdentifierFunction(): void
+ {
+ $functions = $this->adapter->getHelpers(Adapter::FUNCTION_QUOTE_IDENTIFIER);
+
+ self::assertCount(1, $functions);
+ self::assertIsCallable($functions[0]);
+ }
+
+ public function testGetHelpersReturnsQuoteValueFunction(): void
+ {
+ $functions = $this->adapter->getHelpers(Adapter::FUNCTION_QUOTE_VALUE);
+
+ self::assertCount(1, $functions);
+ self::assertIsCallable($functions[0]);
+ }
+
+ public function testGetHelpersReturnsBothFunctions(): void
+ {
+ $functions = $this->adapter->getHelpers(
+ Adapter::FUNCTION_QUOTE_IDENTIFIER,
+ Adapter::FUNCTION_QUOTE_VALUE
+ );
+
+ self::assertCount(2, $functions);
+ self::assertIsCallable($functions[0]);
+ self::assertIsCallable($functions[1]);
+ }
+
+ public function testConstructorWithProfilerDelegatesToSetProfiler(): void
+ {
+ $profilerMock = $this->createMock(Profiler\ProfilerInterface::class);
+ $driverMock = $this->createMock(DriverInterface::class);
+ $platformMock = $this->createMock(PlatformInterface::class);
+
+ $adapter = new Adapter(
+ driver: $driverMock,
+ platform: $platformMock,
+ profiler: $profilerMock,
+ );
+
+ self::assertSame($profilerMock, $adapter->getProfiler());
+ }
+
+ public function testQueryThrowsOnInvalidParameterType(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Parameter 2 to this method must be a flag, an array, or ParameterContainer');
+
+ $this->adapter->query('SELECT 1', 'invalid_mode');
+ }
+
+ public function testSetProfilerDelegatesToDriverWhenProfilerAware(): void
+ {
+ $profiler = $this->createMock(Profiler\ProfilerInterface::class);
+ $driver = $this->createMockForIntersectionOfInterfaces(
+ [DriverInterface::class, Profiler\ProfilerAwareInterface::class]
+ );
+ $driver->expects($this->once())->method('setProfiler')->with($profiler);
+
+ $platform = $this->createMock(PlatformInterface::class);
+ $adapter = new Adapter(driver: $driver, platform: $platform);
+
+ $adapter->setProfiler($profiler);
+ }
+
+ public function testGetHelpersQuoteIdentifierClosureCallsPlatform(): void
+ {
+ $this->mockPlatform->method('quoteIdentifier')
+ ->with('test')
+ ->willReturn('"test"');
+
+ $functions = $this->adapter->getHelpers(Adapter::FUNCTION_QUOTE_IDENTIFIER);
+ $result = $functions[0]('test');
+
+ self::assertSame('"test"', $result);
+ }
+
+ public function testGetHelpersQuoteValueClosureCallsPlatform(): void
+ {
+ $this->mockPlatform->method('quoteValue')
+ ->with('test')
+ ->willThrowException(VunerablePlatformQuoteException::forPlatformAndMethod('test', 'test'));
+
+ $functions = $this->adapter->getHelpers(Adapter::FUNCTION_QUOTE_VALUE);
+
+ $this->expectException(VunerablePlatformQuoteException::class);
+ $functions[0]('test');
+ }
}
diff --git a/test/unit/Adapter/Container/AbstractAdapterInterfaceFactoryTest.php b/test/unit/Adapter/Container/AbstractAdapterInterfaceFactoryTest.php
index 85e8348f..577fe7bc 100644
--- a/test/unit/Adapter/Container/AbstractAdapterInterfaceFactoryTest.php
+++ b/test/unit/Adapter/Container/AbstractAdapterInterfaceFactoryTest.php
@@ -10,15 +10,25 @@
use PhpDb\Adapter\AdapterInterface;
use PhpDb\Adapter\Driver\PdoDriverInterface;
use PhpDb\Adapter\Platform\PlatformInterface;
+use PhpDb\Adapter\Profiler\ProfilerInterface;
use PhpDb\Container\AbstractAdapterInterfaceFactory;
+use PhpDb\Exception\ContainerException;
+use PhpDb\ResultSet\ResultSet;
+use PhpDb\ResultSet\ResultSetInterface;
use PhpDbTest\TestAsset\PdoStubDriver;
+use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
+#[Group('unit')]
+#[CoversMethod(AbstractAdapterInterfaceFactory::class, 'canCreate')]
+#[CoversMethod(AbstractAdapterInterfaceFactory::class, '__invoke')]
+#[CoversMethod(AbstractAdapterInterfaceFactory::class, 'getConfig')]
final class AbstractAdapterInterfaceFactoryTest extends TestCase
{
private ContainerInterface|ServiceManager $serviceManager;
@@ -99,4 +109,98 @@ public function testInvalidService(string $service): void
$this->expectException(ServiceNotFoundException::class);
$this->serviceManager->get($service);
}
+
+ public function testCanCreateReturnsFalseForEmptyConfig(): void
+ {
+ $container = new ServiceManager();
+ $container->setService('config', []);
+
+ $factory = new AbstractAdapterInterfaceFactory();
+
+ self::assertFalse($factory->canCreate($container, 'PhpDb\Adapter\Writer'));
+ }
+
+ public function testInvokeThrowsWhenDriverNotConfigured(): void
+ {
+ $container = new ServiceManager();
+ $container->setService('config', [
+ AdapterInterface::class => [
+ 'adapters' => [
+ 'PhpDb\Adapter\NoDriver' => [],
+ ],
+ ],
+ ]);
+
+ $factory = new AbstractAdapterInterfaceFactory();
+ $factory->canCreate($container, 'PhpDb\Adapter\NoDriver');
+
+ $this->expectException(ContainerException::class);
+ $this->expectExceptionMessage('no driver configured');
+ $factory($container, 'PhpDb\Adapter\NoDriver');
+ }
+
+ public function testGetConfigCachesResult(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+ $container->expects(self::once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(true);
+ $container->expects(self::once())
+ ->method('get')
+ ->with('config')
+ ->willReturn([]);
+
+ $factory = new AbstractAdapterInterfaceFactory();
+
+ $factory->canCreate($container, 'anything');
+ $factory->canCreate($container, 'anything');
+ }
+
+ public function testGetConfigReturnsEmptyWhenContainerHasNoConfig(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+ $container->method('has')->with('config')->willReturn(false);
+
+ $factory = new AbstractAdapterInterfaceFactory();
+
+ self::assertFalse($factory->canCreate($container, 'anything'));
+ }
+
+ public function testInvokeUsesResultSetFromContainer(): void
+ {
+ $resultSet = new ResultSet();
+ $profiler = $this->createMock(ProfilerInterface::class);
+
+ /** @var PdoDriverInterface&MockObject $driverMock */
+ $driverMock = $this->createMock(PdoDriverInterface::class);
+ /** @var PlatformInterface&MockObject $platformMock */
+ $platformMock = $this->createMock(PlatformInterface::class);
+
+ $container = new ServiceManager([
+ 'abstract_factories' => [AbstractAdapterInterfaceFactory::class],
+ 'factories' => [
+ PdoStubDriver::class => static fn() => $driverMock,
+ PlatformInterface::class => static fn() => $platformMock,
+ ResultSetInterface::class => static fn() => $resultSet,
+ ProfilerInterface::class => static fn() => $profiler,
+ ],
+ ]);
+
+ $container->setService('config', [
+ AdapterInterface::class => [
+ 'adapters' => [
+ 'MyAdapter' => [
+ 'driver' => PdoStubDriver::class,
+ ],
+ ],
+ ],
+ ]);
+
+ $adapter = $container->get('MyAdapter');
+
+ self::assertInstanceOf(AdapterInterface::class, $adapter);
+ self::assertSame($resultSet, $adapter->getQueryResultSetPrototype());
+ self::assertSame($profiler, $adapter->getProfiler());
+ }
}
diff --git a/test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php b/test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php
index e42c3ae2..d6321a6b 100644
--- a/test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php
+++ b/test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php
@@ -4,7 +4,6 @@
namespace PhpDbTest\Adapter\Container;
-use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
use Laminas\ServiceManager\ServiceManager;
use PhpDb\Adapter\Adapter;
@@ -15,6 +14,8 @@
use PhpDb\Exception\RuntimeException;
use PhpDb\ResultSet\ResultSetInterface;
use PhpDbTest\Adapter\TestAsset\ConcreteAdapterAwareObject;
+use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
@@ -22,6 +23,10 @@
use Psr\Container\NotFoundExceptionInterface;
use stdClass;
+#[Group('unit')]
+#[CoversMethod(AdapterInterfaceDelegator::class, '__construct')]
+#[CoversMethod(AdapterInterfaceDelegator::class, '__set_state')]
+#[CoversMethod(AdapterInterfaceDelegator::class, '__invoke')]
final class AdapterInterfaceDelegatorTest extends TestCase
{
/**
@@ -211,58 +216,43 @@ public function testDelegatorWithServiceManagerAndCustomAdapterName(): void
);
}
- public function testDelegatorWithPluginManager(): void
+ public function testSetStateWithDefaultAdapterName(): void
{
- $this->markTestSkipped(
- 'Test requires factory-based plugin manager configuration to pass options to constructor'
- );
+ $delegator = AdapterInterfaceDelegator::__set_state([]);
- /** @phpstan-ignore deadCode.unreachable */
- $databaseAdapter = new Adapter(
- $this->createMock(DriverInterface::class),
- $this->createMock(PlatformInterface::class),
- $this->createMock(ResultSetInterface::class)
- );
+ self::assertInstanceOf(AdapterInterfaceDelegator::class, $delegator);
+ }
- $container = new ServiceManager([
- 'factories' => [
- AdapterInterface::class => static fn() => $databaseAdapter,
- ],
- ]);
+ public function testSetStateWithCustomAdapterName(): void
+ {
+ $delegator = AdapterInterfaceDelegator::__set_state(['adapterName' => 'custom']);
- $pluginManagerConfig = [
- 'invokables' => [
- ConcreteAdapterAwareObject::class => ConcreteAdapterAwareObject::class,
- ],
- 'delegators' => [
- ConcreteAdapterAwareObject::class => [
- AdapterInterfaceDelegator::class,
- ],
- ],
- ];
+ self::assertInstanceOf(AdapterInterfaceDelegator::class, $delegator);
+ }
- /** @var AbstractPluginManager $pluginManager */
- $pluginManager = new class ($container, $pluginManagerConfig) extends AbstractPluginManager {
- public function validate(mixed $instance): void
- {
- }
- };
+ public function testInvokeReturnsInstanceWhenAdapterIsNotAdapterInterface(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+ $container
+ ->expects(self::once())
+ ->method('has')
+ ->with(AdapterInterface::class)
+ ->willReturn(true);
+ $container
+ ->expects(self::once())
+ ->method('get')
+ ->with(AdapterInterface::class)
+ ->willReturn(new stdClass());
- $options = [
- 'table' => 'foo',
- 'field' => 'bar',
- ];
+ $callback = static fn(): ConcreteAdapterAwareObject => new ConcreteAdapterAwareObject();
- /** @var ConcreteAdapterAwareObject $result */
- $result = $pluginManager->get(
+ $result = (new AdapterInterfaceDelegator())(
+ $container,
ConcreteAdapterAwareObject::class,
- $options
+ $callback
);
- $this->assertInstanceOf(
- AdapterInterface::class,
- $result->getAdapter()
- );
- $this->assertSame($options, $result->getOptions());
+ self::assertInstanceOf(ConcreteAdapterAwareObject::class, $result);
+ self::assertNull($result->getAdapter());
}
}
diff --git a/test/unit/Adapter/Container/TestAsset/TestPluginManager.php b/test/unit/Adapter/Container/TestAsset/TestPluginManager.php
new file mode 100644
index 00000000..3a746496
--- /dev/null
+++ b/test/unit/Adapter/Container/TestAsset/TestPluginManager.php
@@ -0,0 +1,14 @@
+connect();
+
+ self::assertTrue($connection->isConnected());
+
+ $connection->disconnect();
+
+ self::assertFalse($connection->isConnected());
+ }
+
+ public function testDisconnectIsNoOpWhenNotConnected(): void
+ {
+ $connection = new TestConnection();
+
+ $result = $connection->disconnect();
+
+ self::assertSame($connection, $result);
+ }
+
+ public function testGetConnectionParametersReturnsEmptyByDefault(): void
+ {
+ $connection = new TestConnection();
+
+ self::assertSame([], $connection->getConnectionParameters());
+ }
+
+ public function testGetDriverNameReturnsValueWhenSet(): void
+ {
+ $connection = new TestConnection();
+ $connection->setConnectionParameters(['driver' => 'sqlite']);
+
+ self::assertNull($connection->getDriverName());
+ }
+
+ public function testGetProfilerReturnsNullByDefault(): void
+ {
+ $connection = new TestConnection();
+
+ self::assertNull($connection->getProfiler());
+ }
+
+ public function testSetProfilerStoresAndReturnsProfiler(): void
+ {
+ $connection = new TestConnection();
+ $profiler = $this->createMock(ProfilerInterface::class);
+
+ $result = $connection->setProfiler($profiler);
+
+ self::assertSame($connection, $result);
+ self::assertSame($profiler, $connection->getProfiler());
+ }
+
+ public function testGetResourceAutoConnectsWhenNotConnected(): void
+ {
+ $connection = new TestConnection();
+
+ self::assertFalse($connection->isConnected());
+
+ $resource = $connection->getResource();
+
+ self::assertTrue($connection->isConnected());
+ self::assertSame('fake-resource', $resource);
+ }
+
+ public function testSetConnectionParametersStoresAndReturnsConnection(): void
+ {
+ $connection = new TestConnection();
+ $params = ['host' => 'localhost', 'port' => 3306];
+
+ $result = $connection->setConnectionParameters($params);
+
+ self::assertSame($connection, $result);
+ self::assertSame($params, $connection->getConnectionParameters());
+ }
+
+ public function testInTransactionReturnsFalseByDefault(): void
+ {
+ $connection = new TestConnection();
+
+ self::assertFalse($connection->inTransaction());
+ }
+}
diff --git a/test/unit/Adapter/Driver/Feature/AbstractFeatureTest.php b/test/unit/Adapter/Driver/Feature/AbstractFeatureTest.php
new file mode 100644
index 00000000..f0839b2d
--- /dev/null
+++ b/test/unit/Adapter/Driver/Feature/AbstractFeatureTest.php
@@ -0,0 +1,29 @@
+createMock(DriverInterface::class);
+
+ $result = $feature->setDriver($driver);
+
+ self::assertInstanceOf(DriverFeatureInterface::class, $result);
+ self::assertSame($feature, $result);
+ }
+}
diff --git a/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php b/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php
new file mode 100644
index 00000000..bd94d140
--- /dev/null
+++ b/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php
@@ -0,0 +1,74 @@
+createMock(DriverFeatureInterface::class);
+ $feature->expects(self::once())->method('setDriver')->with($driver);
+
+ $driver->addFeature($feature);
+
+ self::assertSame($feature, $driver->getFeature($feature::class));
+ }
+
+ public function testAddFeaturesAddsMultipleFeatures(): void
+ {
+ $driver = new TestFeatureDriver();
+ $feature1 = $this->createMock(DriverFeatureInterface::class);
+ $feature2 = $this->createMock(DriverFeatureInterface::class);
+
+ $driver->addFeatures([$feature1, $feature2]);
+
+ self::assertNotFalse($driver->getFeature($feature1::class));
+ }
+
+ public function testGetFeatureReturnsFeatureByClassName(): void
+ {
+ $driver = new TestFeatureDriver();
+ $feature = $this->createMock(DriverFeatureInterface::class);
+
+ $driver->addFeature($feature);
+
+ self::assertSame($feature, $driver->getFeature($feature::class));
+ }
+
+ public function testGetFeatureReturnsFalseWhenNotFound(): void
+ {
+ $driver = new TestFeatureDriver();
+
+ self::assertFalse($driver->getFeature('NonExistent'));
+ }
+
+ public function testAddFeatureThrowsWhenUsedOutsideDriverInterface(): void
+ {
+ $nonDriver = new class implements DriverFeatureProviderInterface {
+ use DriverFeatureProviderTrait;
+ };
+
+ $feature = $this->createMock(DriverFeatureInterface::class);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('can only be composed into');
+
+ $nonDriver->addFeature($feature);
+ }
+}
diff --git a/test/unit/Adapter/Driver/Feature/TestAsset/TestDriverFeature.php b/test/unit/Adapter/Driver/Feature/TestAsset/TestDriverFeature.php
new file mode 100644
index 00000000..62a9e8c6
--- /dev/null
+++ b/test/unit/Adapter/Driver/Feature/TestAsset/TestDriverFeature.php
@@ -0,0 +1,11 @@
+markTestSkipped('Test requires concrete driver implementation with DSN building logic');
- /** @phpstan-ignore deadCode.unreachable */
- $this->expectException(InvalidConnectionParametersException::class);
- $this->connection->getResource();
- }
-
/**
* Test getConnectedDsn returns a DSN string if it has been set
*/
@@ -62,71 +69,131 @@ public function testGetDsn(): void
self::assertEquals($dsn, $responseString);
}
- #[Group('2622')]
- public function testArrayOfConnectionParametersCreatesCorrectDsn(): void
+ public function testConstructorWithPdoResourceSetsConnected(): void
{
- $this->markTestSkipped('Test requires concrete MySQL driver implementation with DSN building logic');
- /** @phpstan-ignore deadCode.unreachable */
- $this->connection->setConnectionParameters([
- 'driver' => 'pdo_mysql',
- 'charset' => 'utf8',
- 'dbname' => 'foo',
- 'port' => '3306',
- 'unix_socket' => '/var/run/mysqld/mysqld.sock',
- ]);
- try {
- $this->connection->connect();
- } catch (Exception) {
- }
- $responseString = $this->connection->getDsn();
+ $pdo = new SqliteMemoryPdo();
+ $connection = new TestConnection($pdo);
- self::assertStringStartsWith('mysql:', $responseString);
- self::assertStringContainsString('charset=utf8', $responseString);
- self::assertStringContainsString('dbname=foo', $responseString);
- self::assertStringContainsString('port=3306', $responseString);
- self::assertStringContainsString('unix_socket=/var/run/mysqld/mysqld.sock', $responseString);
+ self::assertTrue($connection->isConnected());
}
- public function testHostnameAndUnixSocketThrowsInvalidConnectionParametersException(): void
+ public function testConstructorWithArraySetsConnectionParameters(): void
{
- $this->markTestSkipped('Test requires concrete MySQL driver implementation with parameter validation');
- /** @phpstan-ignore deadCode.unreachable */
- $this->expectException(InvalidConnectionParametersException::class);
- $this->expectExceptionMessage(
- 'Ambiguous connection parameters, both hostname and unix_socket parameters were set'
- );
+ $params = ['dsn' => 'sqlite::memory:', 'username' => 'user'];
+ $connection = new TestConnection($params);
- $this->connection->setConnectionParameters([
- 'driver' => 'pdo_mysql',
- 'host' => '127.0.0.1',
- 'dbname' => 'foo',
- 'port' => '3306',
- 'unix_socket' => '/var/run/mysqld/mysqld.sock',
- ]);
- $this->connection->connect();
+ self::assertSame($params, $connection->getConnectionParameters());
}
- public function testDblibArrayOfConnectionParametersCreatesCorrectDsn(): void
+ public function testSetDriverReturnsInstance(): void
{
- $this->markTestSkipped('Test requires concrete Dblib driver implementation with DSN building logic');
- /** @phpstan-ignore deadCode.unreachable */
- $this->connection->setConnectionParameters([
- 'driver' => 'pdo_dblib',
- 'charset' => 'UTF-8',
- 'dbname' => 'foo',
- 'port' => '1433',
- 'version' => '7.3',
- ]);
- try {
- $this->connection->connect();
- } catch (Exception) {
- }
- $responseString = $this->connection->getDsn();
+ $driver = $this->createMock(PdoDriverInterface::class);
+
+ $result = $this->connection->setDriver($driver);
+
+ self::assertSame($this->connection, $result);
+ }
+
+ public function testSetConnectionParametersStoresParams(): void
+ {
+ $params = ['dsn' => 'sqlite::memory:', 'username' => 'test'];
+
+ $this->connection->setConnectionParameters($params);
+
+ self::assertSame($params, $this->connection->getConnectionParameters());
+ }
+
+ public function testExecuteCallsProfilerStartAndFinish(): void
+ {
+ $profiler = $this->createMock(ProfilerInterface::class);
+ $profiler->expects($this->once())->method('profilerStart')->willReturnSelf();
+ $profiler->expects($this->once())->method('profilerFinish')->willReturnSelf();
+
+ $pdo = new SqliteMemoryPdo();
+ $connection = new TestConnection($pdo);
+ $driver = new TestPdo($connection);
+ $connection->setProfiler($profiler);
+
+ $connection->execute('SELECT 1');
+ }
+
+ public function testExecuteCallsProfilerFinishBeforeThrowingOnError(): void
+ {
+ $profiler = $this->createMock(ProfilerInterface::class);
+ $profiler->expects($this->once())->method('profilerStart')->willReturnSelf();
+ $profiler->expects($this->once())->method('profilerFinish')->willReturnSelf();
+
+ $pdo = new SqliteMemoryPdo();
+ $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
+ $connection = new TestConnection($pdo);
+ $driver = new TestPdo($connection);
+ $connection->setProfiler($profiler);
+
+ $this->expectException(InvalidQueryException::class);
+ @$connection->execute('INVALID SQL STATEMENT HERE %%%');
+ }
+
+ public function testGetDsnThrowsWhenDsnIsNull(): void
+ {
+ $connection = new TestConnection(['dsn' => 'sqlite::memory:']);
+ $connection->connect();
+
+ $reflection = new ReflectionProperty($connection, 'dsn');
+ $reflection->setValue($connection, null);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('The DSN has not been set');
+
+ $connection->getDsn();
+ }
+
+ public function testBeginTransactionAutoConnectsWhenNotConnected(): void
+ {
+ $connection = new TestConnection(['dsn' => 'sqlite::memory:']);
+
+ self::assertFalse($connection->isConnected());
+
+ $connection->beginTransaction();
+
+ self::assertTrue($connection->isConnected());
+ }
+
+ public function testCommitAutoConnectsWhenNotConnected(): void
+ {
+ $connection = new TestConnection(new SqliteMemoryPdo());
+ $connection->beginTransaction();
+ $connection->beginTransaction();
+ $connection->disconnect();
+
+ self::assertFalse($connection->isConnected());
+
+ $connection->commit();
+
+ self::assertTrue($connection->isConnected());
+ }
+
+ public function testExecuteAutoConnectsWhenNotConnected(): void
+ {
+ $connection = new TestConnection(['dsn' => 'sqlite::memory:']);
+ $driver = new TestPdo($connection);
+
+ self::assertFalse($connection->isConnected());
+
+ $connection->execute('SELECT 1');
+
+ self::assertTrue($connection->isConnected());
+ }
+
+ public function testPrepareAutoConnectsAndReturnsStatement(): void
+ {
+ $connection = new TestConnection(['dsn' => 'sqlite::memory:']);
+ $driver = new TestPdo($connection);
+
+ self::assertFalse($connection->isConnected());
+
+ $statement = $connection->prepare('SELECT 1');
- $this->assertStringStartsWith('dblib:', $responseString);
- $this->assertStringContainsString('charset=UTF-8', $responseString);
- $this->assertStringContainsString('dbname=foo', $responseString);
- $this->assertStringContainsString('port=1433', $responseString);
- $this->assertStringContainsString('version=7.3', $responseString);
+ self::assertTrue($connection->isConnected());
+ self::assertInstanceOf(Statement::class, $statement);
}
}
diff --git a/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php b/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php
index 0eddf353..c3e22318 100644
--- a/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php
+++ b/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php
@@ -19,10 +19,10 @@
*/
#[CoversClass(AbstractPdoConnection::class)]
#[CoversClass(AbstractConnection::class)]
-#[CoversMethod(AbstractPdoConnection::class, 'beginTransaction()')]
-#[CoversMethod(AbstractConnection::class, 'inTransaction()')]
-#[CoversMethod(AbstractPdoConnection::class, 'commit()')]
-#[CoversMethod(AbstractPdoConnection::class, 'rollback()')]
+#[CoversMethod(AbstractPdoConnection::class, 'beginTransaction')]
+#[CoversMethod(AbstractConnection::class, 'inTransaction')]
+#[CoversMethod(AbstractPdoConnection::class, 'commit')]
+#[CoversMethod(AbstractPdoConnection::class, 'rollback')]
final class ConnectionTransactionsTest extends TestCase
{
protected ConnectionWrapper $wrapper;
diff --git a/test/unit/Adapter/Driver/Pdo/PdoTest.php b/test/unit/Adapter/Driver/Pdo/PdoTest.php
index 0b1e80d4..77695281 100644
--- a/test/unit/Adapter/Driver/Pdo/PdoTest.php
+++ b/test/unit/Adapter/Driver/Pdo/PdoTest.php
@@ -4,18 +4,37 @@
namespace PhpDbTest\Adapter\Driver\Pdo;
+use Error;
use Override;
+use PDOStatement;
use PhpDb\Adapter\Driver\DriverInterface;
+use PhpDb\Adapter\Driver\Feature\DriverFeatureInterface;
use PhpDb\Adapter\Driver\Pdo\AbstractPdo;
use PhpDb\Adapter\Driver\Pdo\Result;
+use PhpDb\Adapter\Driver\Pdo\Statement;
+use PhpDb\Adapter\Profiler\ProfilerInterface;
use PhpDb\Exception\RuntimeException;
+use PhpDbTest\Adapter\Driver\Pdo\TestAsset\SqliteMemoryPdo;
+use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestConnection;
use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestPdo;
+use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestPdoWithFeatures;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[CoversMethod(AbstractPdo::class, 'getDatabasePlatformName')]
#[CoversMethod(AbstractPdo::class, 'getResultPrototype')]
+#[CoversMethod(AbstractPdo::class, '__construct')]
+#[CoversMethod(AbstractPdo::class, 'checkEnvironment')]
+#[CoversMethod(AbstractPdo::class, 'getConnection')]
+#[CoversMethod(AbstractPdo::class, 'createStatement')]
+#[CoversMethod(AbstractPdo::class, 'getPrepareType')]
+#[CoversMethod(AbstractPdo::class, 'getLastGeneratedValue')]
+#[CoversMethod(AbstractPdo::class, 'setProfiler')]
+#[CoversMethod(AbstractPdo::class, 'getProfiler')]
+#[CoversMethod(AbstractPdo::class, 'formatParameterName')]
+#[Group('unit')]
final class PdoTest extends TestCase
{
protected TestPdo $pdo;
@@ -87,4 +106,119 @@ public function testGetResultPrototype(): void
self::assertInstanceOf(Result::class, $resultPrototype);
}
+
+ public function testConstructorSetsDriverOnConnection(): void
+ {
+ $connection = new TestConnection(new SqliteMemoryPdo());
+ $pdo = new TestPdo($connection);
+
+ self::assertSame($connection, $pdo->getConnection());
+ }
+
+ public function testCheckEnvironmentReturnsTrue(): void
+ {
+ self::assertTrue($this->pdo->checkEnvironment());
+ }
+
+ public function testGetConnectionReturnsConnectionInstance(): void
+ {
+ $connection = $this->pdo->getConnection();
+
+ self::assertInstanceOf(TestConnection::class, $connection);
+ }
+
+ public function testCreateStatementWithSqlString(): void
+ {
+ $connection = new TestConnection(new SqliteMemoryPdo());
+ $pdo = new TestPdo($connection);
+
+ $statement = $pdo->createStatement('SELECT 1');
+
+ self::assertInstanceOf(Statement::class, $statement);
+ self::assertSame('SELECT 1', $statement->getSql());
+ }
+
+ public function testCreateStatementWithNullConnectsAndInitializes(): void
+ {
+ $connection = new TestConnection(['dsn' => 'sqlite::memory:']);
+ $pdo = new TestPdo($connection);
+
+ $statement = $pdo->createStatement();
+
+ self::assertInstanceOf(Statement::class, $statement);
+ }
+
+ public function testGetPrepareTypeReturnsNamed(): void
+ {
+ self::assertSame(DriverInterface::PARAMETERIZATION_NAMED, $this->pdo->getPrepareType());
+ }
+
+ public function testGetLastGeneratedValueDelegatesToConnection(): void
+ {
+ $connection = new TestConnection(new SqliteMemoryPdo());
+ $pdo = new TestPdo($connection);
+
+ $value = $pdo->getLastGeneratedValue();
+
+ self::assertSame('0', $value);
+ }
+
+ public function testSetProfilerPropagatesProfilerToConnectionAndStatement(): void
+ {
+ $profiler = $this->createMock(ProfilerInterface::class);
+ $connection = new TestConnection(new SqliteMemoryPdo());
+ $statement = new Statement();
+ $pdo = new TestPdo($connection, $statement);
+
+ $pdo->setProfiler($profiler);
+
+ self::assertSame($profiler, $pdo->getProfiler());
+ self::assertSame($profiler, $connection->getProfiler());
+ self::assertSame($profiler, $statement->getProfiler());
+ }
+
+ public function testGetProfilerThrowsWhenNotInitialized(): void
+ {
+ $pdo = new TestPdo([]);
+
+ $this->expectException(Error::class);
+ /** @phpstan-ignore method.resultUnused */
+ $pdo->getProfiler();
+ }
+
+ public function testGetProfilerReturnsSetProfiler(): void
+ {
+ $profiler = $this->createMock(ProfilerInterface::class);
+
+ $this->pdo->setProfiler($profiler);
+
+ self::assertSame($profiler, $this->pdo->getProfiler());
+ }
+
+ public function testConstructorAddsFeaturesWhenDriverSupportsFeatures(): void
+ {
+ $feature = $this->createMock(DriverFeatureInterface::class);
+ $connection = new TestConnection(new SqliteMemoryPdo());
+
+ $pdo = new TestPdoWithFeatures($connection, features: [$feature]);
+
+ self::assertSame($feature, $pdo->getFeature($feature::class));
+ }
+
+ public function testCreateStatementWithPdoStatementResource(): void
+ {
+ $connection = new TestConnection(new SqliteMemoryPdo());
+ $pdo = new TestPdo($connection);
+
+ $pdoStmt = $this->createMock(PDOStatement::class);
+ $statement = $pdo->createStatement($pdoStmt);
+
+ self::assertInstanceOf(Statement::class, $statement);
+ self::assertSame($pdoStmt, $statement->getResource());
+ }
+
+ public function testFormatParameterNameReturnsQuestionMarkForNumericWithoutType(): void
+ {
+ self::assertSame('?', $this->pdo->formatParameterName(42));
+ }
}
diff --git a/test/unit/Adapter/Driver/Pdo/ResultTest.php b/test/unit/Adapter/Driver/Pdo/ResultTest.php
index 1044874a..e81d4ac8 100644
--- a/test/unit/Adapter/Driver/Pdo/ResultTest.php
+++ b/test/unit/Adapter/Driver/Pdo/ResultTest.php
@@ -8,6 +8,7 @@
use PDOStatement;
use PhpDb\Adapter\Driver\Pdo\Result;
use PhpDb\Adapter\Exception\InvalidArgumentException;
+use PhpDb\Adapter\Exception\RuntimeException;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
@@ -18,7 +19,24 @@
#[CoversMethod(Result::class, 'current')]
#[CoversMethod(Result::class, 'count')]
+#[CoversMethod(Result::class, 'initialize')]
+#[CoversMethod(Result::class, 'isBuffered')]
+#[CoversMethod(Result::class, 'getFetchMode')]
+#[CoversMethod(Result::class, 'setStatementMode')]
+#[CoversMethod(Result::class, 'getStatementMode')]
+#[CoversMethod(Result::class, 'getResource')]
+#[CoversMethod(Result::class, 'getFieldCount')]
+#[CoversMethod(Result::class, 'isQueryResult')]
+#[CoversMethod(Result::class, 'getAffectedRows')]
+#[CoversMethod(Result::class, 'getGeneratedValue')]
+#[CoversMethod(Result::class, 'rewind')]
+#[CoversMethod(Result::class, 'next')]
+#[CoversMethod(Result::class, 'key')]
+#[CoversMethod(Result::class, 'buffer')]
+#[CoversMethod(Result::class, 'setFetchMode')]
+#[CoversMethod(Result::class, 'valid')]
#[Group('result-pdo')]
+#[Group('unit')]
final class ResultTest extends TestCase
{
/**
@@ -179,4 +197,205 @@ public function testCountCachesResultFromStatementRowCount(): void
self::assertSame(3, $result->count());
}
+
+ public function testInitializeStoresResourceAndValues(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $result = new Result();
+
+ $result->initialize($stub, 42, 5);
+
+ self::assertSame(42, $result->getGeneratedValue());
+ self::assertSame(5, $result->count());
+ }
+
+ public function testIsBufferedReturnsFalse(): void
+ {
+ $result = new Result();
+
+ self::assertFalse($result->isBuffered());
+ }
+
+ public function testGetFetchModeDefaultIsAssoc(): void
+ {
+ $result = new Result();
+
+ self::assertSame(PDO::FETCH_ASSOC, $result->getFetchMode());
+ }
+
+ public function testSetStatementModeToScrollable(): void
+ {
+ $result = new Result();
+
+ $result->setStatementMode(Result::STATEMENT_MODE_SCROLLABLE);
+
+ self::assertSame(Result::STATEMENT_MODE_SCROLLABLE, $result->getStatementMode());
+ }
+
+ public function testSetStatementModeToForward(): void
+ {
+ $result = new Result();
+
+ $result->setStatementMode(Result::STATEMENT_MODE_FORWARD);
+
+ self::assertSame(Result::STATEMENT_MODE_FORWARD, $result->getStatementMode());
+ }
+
+ public function testSetStatementModeThrowsOnInvalidMode(): void
+ {
+ $result = new Result();
+
+ $this->expectException(InvalidArgumentException::class);
+ $result->setStatementMode('invalid');
+ }
+
+ public function testGetResourceReturnsPdoStatement(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $result = new Result();
+ $result->initialize($stub, null);
+
+ self::assertSame($stub, $result->getResource());
+ }
+
+ public function testGetFieldCountDelegatesToColumnCount(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $stub->method('columnCount')->willReturn(3);
+
+ $result = new Result();
+ $result->initialize($stub, null);
+
+ self::assertSame(3, $result->getFieldCount());
+ }
+
+ public function testIsQueryResultReturnsTrueWhenColumnsExist(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $stub->method('columnCount')->willReturn(3);
+
+ $result = new Result();
+ $result->initialize($stub, null);
+
+ self::assertTrue($result->isQueryResult());
+ }
+
+ public function testIsQueryResultReturnsFalseWhenNoColumns(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $stub->method('columnCount')->willReturn(0);
+
+ $result = new Result();
+ $result->initialize($stub, null);
+
+ self::assertFalse($result->isQueryResult());
+ }
+
+ public function testGetAffectedRowsDelegatesToRowCount(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $stub->method('rowCount')->willReturn(5);
+
+ $result = new Result();
+ $result->initialize($stub, null);
+
+ self::assertSame(5, $result->getAffectedRows());
+ }
+
+ public function testGetGeneratedValueReturnsInitializedValue(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $result = new Result();
+ $result->initialize($stub, 42);
+
+ self::assertSame(42, $result->getGeneratedValue());
+ }
+
+ public function testGetGeneratedValueReturnsNullByDefault(): void
+ {
+ $result = new Result();
+
+ self::assertNull($result->getGeneratedValue());
+ }
+
+ public function testRewindThrowsExceptionOnForwardOnlyAfterAdvancing(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $stub->method('fetch')->willReturn(['id' => 1]);
+
+ $result = new Result();
+ $result->initialize($stub, null);
+ $result->setStatementMode(Result::STATEMENT_MODE_FORWARD);
+
+ $result->rewind();
+ $result->next();
+
+ $this->expectException(RuntimeException::class);
+ $result->rewind();
+ }
+
+ public function testNextAdvancesPositionAndFetchesData(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $stub->method('fetch')->willReturn(['name' => 'test']);
+
+ $result = new Result();
+ $result->initialize($stub, null);
+
+ $result->rewind();
+ self::assertSame(0, $result->key());
+
+ $result->next();
+ self::assertSame(1, $result->key());
+ }
+
+ public function testBufferIsCallableWithNoEffect(): void
+ {
+ $result = new Result();
+ $result->buffer();
+
+ self::assertFalse($result->isBuffered());
+ }
+
+ public function testSetFetchModeThrowsOnInvalidFetchMode(): void
+ {
+ $result = new Result();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The fetch mode must be one of the PDO::FETCH_* constants.');
+
+ $result->setFetchMode(9999);
+ }
+
+ public function testSetFetchModeStoresValidMode(): void
+ {
+ $result = new Result();
+ $result->setFetchMode(PDO::FETCH_NUM);
+
+ self::assertSame(PDO::FETCH_NUM, $result->getFetchMode());
+ }
+
+ public function testValidReturnsFalseWhenCurrentDataIsFalse(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $stub->method('fetch')->willReturn(false);
+
+ $result = new Result();
+ $result->initialize($stub, null);
+ $result->rewind();
+
+ self::assertFalse($result->valid());
+ }
+
+ public function testValidReturnsTrueWhenCurrentDataExists(): void
+ {
+ $stub = $this->createMock(PDOStatement::class);
+ $stub->method('fetch')->willReturn(['id' => 1]);
+
+ $result = new Result();
+ $result->initialize($stub, null);
+ $result->rewind();
+
+ self::assertTrue($result->valid());
+ }
}
diff --git a/test/unit/Adapter/Driver/Pdo/StatementTest.php b/test/unit/Adapter/Driver/Pdo/StatementTest.php
index c95ba6b4..b70d3816 100644
--- a/test/unit/Adapter/Driver/Pdo/StatementTest.php
+++ b/test/unit/Adapter/Driver/Pdo/StatementTest.php
@@ -5,15 +5,23 @@
namespace PhpDbTest\Adapter\Driver\Pdo;
use Override;
+use PDO;
+use PDOException;
+use PDOStatement;
use PhpDb\Adapter\Driver\Pdo\Result;
use PhpDb\Adapter\Driver\Pdo\Statement;
+use PhpDb\Adapter\Exception\InvalidQueryException;
use PhpDb\Adapter\Exception\RuntimeException;
use PhpDb\Adapter\ParameterContainer;
+use PhpDb\Adapter\Profiler\ProfilerInterface;
+use PhpDbTest\Adapter\Driver\Pdo\TestAsset\SqliteMemoryPdo;
use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestConnection;
use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestPdo;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
+use ReflectionProperty;
#[CoversMethod(Statement::class, 'setDriver')]
#[CoversMethod(Statement::class, 'setParameterContainer')]
@@ -25,6 +33,13 @@
#[CoversMethod(Statement::class, 'isPrepared')]
#[CoversMethod(Statement::class, 'execute')]
#[CoversMethod(Statement::class, 'bindParametersFromContainer')]
+#[CoversMethod(Statement::class, 'setProfiler')]
+#[CoversMethod(Statement::class, 'getProfiler')]
+#[CoversMethod(Statement::class, 'initialize')]
+#[CoversMethod(Statement::class, 'setResource')]
+#[CoversMethod(Statement::class, '__clone')]
+#[CoversMethod(Statement::class, '__construct')]
+#[Group('unit')]
final class StatementTest extends TestCase
{
protected Statement $statement;
@@ -69,7 +84,7 @@ public function testGetParameterContainer(): void
public function testGetResource(): void
{
- $pdo = new TestAsset\SqliteMemoryPdo();
+ $pdo = new SqliteMemoryPdo();
$stmt = $pdo->prepare('SELECT 1');
$this->statement->setResource($stmt);
@@ -93,7 +108,7 @@ public function testGetSql(): void
*/
public function testPrepare(): void
{
- $this->statement->initialize(new TestAsset\SqliteMemoryPdo());
+ $this->statement->initialize(new SqliteMemoryPdo());
$result = $this->statement->prepare('SELECT 1');
self::assertInstanceOf(Statement::class, $result);
self::assertSame($this->statement, $result);
@@ -102,14 +117,14 @@ public function testPrepare(): void
public function testIsPrepared(): void
{
self::assertFalse($this->statement->isPrepared());
- $this->statement->initialize(new TestAsset\SqliteMemoryPdo());
+ $this->statement->initialize(new SqliteMemoryPdo());
$this->statement->prepare('SELECT 1');
self::assertTrue($this->statement->isPrepared());
}
public function testExecute(): void
{
- $this->statement->setDriver(new TestPdo(new TestConnection($pdo = new TestAsset\SqliteMemoryPdo())));
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo = new SqliteMemoryPdo())));
$this->statement->initialize($pdo);
$this->statement->prepare('SELECT 1');
self::assertInstanceOf(Result::class, $this->statement->execute());
@@ -131,7 +146,7 @@ public static function invalidParameterNameProvider(): array
#[DataProvider('invalidParameterNameProvider')]
public function testExecuteThrowsOnInvalidParameterName(string $name): void
{
- $this->statement->setDriver(new TestPdo(new TestConnection($pdo = new TestAsset\SqliteMemoryPdo())));
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo = new SqliteMemoryPdo())));
$this->statement->initialize($pdo);
$this->statement->prepare('SELECT 1');
@@ -139,4 +154,379 @@ public function testExecuteThrowsOnInvalidParameterName(string $name): void
$this->expectExceptionMessage('contains invalid characters');
$this->statement->execute([$name => 'value']);
}
+
+ public function testSetProfilerStoresProfiler(): void
+ {
+ $profiler = $this->createMock(ProfilerInterface::class);
+
+ $this->statement->setProfiler($profiler);
+
+ self::assertSame($profiler, $this->statement->getProfiler());
+ }
+
+ public function testGetProfilerReturnsNullByDefault(): void
+ {
+ self::assertNull($this->statement->getProfiler());
+ }
+
+ public function testInitializeSetsPdoResource(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT 1');
+ $this->statement->prepare();
+
+ self::assertTrue($this->statement->isPrepared());
+ }
+
+ public function testSetResourceStoresPdoStatement(): void
+ {
+ $pdoStmt = $this->createMock(PDOStatement::class);
+
+ $this->statement->setResource($pdoStmt);
+
+ self::assertSame($pdoStmt, $this->statement->getResource());
+ }
+
+ public function testPrepareThrowsRuntimeExceptionOnPdoFailure(): void
+ {
+ $pdo = $this->createMock(PDO::class);
+ $pdo->method('prepare')->willReturn(false);
+ $pdo->method('errorInfo')->willReturn(['HY000', 1, 'Prepare failed']);
+
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('INVALID SQL');
+
+ $this->expectException(RuntimeException::class);
+ $this->statement->prepare();
+ }
+
+ public function testExecuteThrowsInvalidQueryExceptionOnPdoException(): void
+ {
+ $pdoStmt = $this->createMock(PDOStatement::class);
+ $pdoStmt->method('execute')->willThrowException(new PDOException('execute failed'));
+ $pdoStmt->method('errorInfo')->willReturn(['HY000', 1, 'execute failed']);
+
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT 1');
+ $this->statement->prepare();
+
+ $reflection = new ReflectionProperty($this->statement, 'resource');
+ $reflection->setValue($this->statement, $pdoStmt);
+
+ $this->expectException(InvalidQueryException::class);
+ $this->statement->execute();
+ }
+
+ public function testExecuteCallsProfilerOnSuccess(): void
+ {
+ $profiler = $this->createMock(ProfilerInterface::class);
+ $profiler->expects($this->once())->method('profilerStart')->willReturnSelf();
+ $profiler->expects($this->once())->method('profilerFinish')->willReturnSelf();
+
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT 1');
+ $this->statement->setProfiler($profiler);
+
+ $this->statement->execute();
+ }
+
+ public function testExecuteCallsProfilerFinishOnFailure(): void
+ {
+ $profiler = $this->createMock(ProfilerInterface::class);
+ $profiler->expects($this->once())->method('profilerStart')->willReturnSelf();
+ $profiler->expects($this->once())->method('profilerFinish')->willReturnSelf();
+
+ $pdoStmt = $this->createMock(PDOStatement::class);
+ $pdoStmt->method('execute')->willThrowException(new PDOException('fail'));
+ $pdoStmt->method('errorInfo')->willReturn(['HY000', 1, 'fail']);
+
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT 1');
+ $this->statement->prepare();
+ $this->statement->setProfiler($profiler);
+
+ $reflection = new ReflectionProperty($this->statement, 'resource');
+ $reflection->setValue($this->statement, $pdoStmt);
+
+ $this->expectException(InvalidQueryException::class);
+ $this->statement->execute();
+ }
+
+ public function testCloneResetsState(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT 1');
+ $this->statement->prepare();
+
+ $clone = clone $this->statement;
+
+ self::assertFalse($clone->isPrepared());
+ self::assertNull($clone->getResource());
+ self::assertNotSame(
+ $this->statement->getParameterContainer(),
+ $clone->getParameterContainer()
+ );
+ }
+
+ public function testBindParametersWithPositionalIntegers(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT ?, ?, ?');
+
+ $container = new ParameterContainer();
+ $container->offsetSet(0, 'a');
+ $container->offsetSet(1, 'b');
+ $container->offsetSet(2, 'c');
+ $this->statement->setParameterContainer($container);
+
+ $result = $this->statement->execute();
+
+ self::assertInstanceOf(Result::class, $result);
+ }
+
+ public function testConstructorAcceptsParameterContainerAndOptions(): void
+ {
+ $container = new ParameterContainer(['key' => 'value']);
+ $statement = new Statement($container, ['option' => true]);
+
+ self::assertSame($container, $statement->getParameterContainer());
+ }
+
+ public function testPrepareThrowsWhenAlreadyPrepared(): void
+ {
+ $this->statement->initialize(new SqliteMemoryPdo());
+ $this->statement->prepare('SELECT 1');
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('This statement has been prepared already');
+
+ $this->statement->prepare('SELECT 2');
+ }
+
+ public function testExecuteWithParameterContainerSetsContainer(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $statement = new Statement();
+ $statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $statement->initialize($pdo);
+ $statement->setSql('SELECT ?');
+
+ $container = new ParameterContainer();
+ $container->offsetSet(null, 'value');
+
+ $result = $statement->execute($container);
+
+ self::assertInstanceOf(Result::class, $result);
+ self::assertSame($container, $statement->getParameterContainer());
+ }
+
+ public function testExecuteWithArrayParametersMergesIntoContainer(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $statement = new Statement();
+ $statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $statement->initialize($pdo);
+ $statement->setSql('SELECT :name');
+
+ $result = $statement->execute(['name' => 'test']);
+
+ self::assertInstanceOf(Result::class, $result);
+ }
+
+ public function testExecuteCastsNonIntErrorCodeToZero(): void
+ {
+ $pdoException = new PDOException('fail');
+ $ref = new ReflectionProperty($pdoException, 'code');
+ $ref->setValue($pdoException, 'HY000');
+
+ $pdoStmt = $this->createMock(PDOStatement::class);
+ $pdoStmt->method('execute')->willThrowException($pdoException);
+ $pdoStmt->method('errorInfo')->willReturn(['HY000', 1, 'fail']);
+
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT 1');
+ $this->statement->prepare();
+
+ $reflection = new ReflectionProperty($this->statement, 'resource');
+ $reflection->setValue($this->statement, $pdoStmt);
+
+ try {
+ $this->statement->execute();
+ self::fail('Expected InvalidQueryException');
+ } catch (InvalidQueryException $e) {
+ self::assertSame(0, $e->getCode());
+ }
+ }
+
+ public function testBindParametersFromContainerSkipsWhenAlreadyBound(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT :val');
+ $this->statement->setParameterContainer(new ParameterContainer(['val' => 'first']));
+
+ $result1 = $this->statement->execute();
+ self::assertInstanceOf(Result::class, $result1);
+ }
+
+ public function testBindParametersWithErrataTypeInteger(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT :val');
+
+ $container = new ParameterContainer();
+ $container->offsetSet('val', 42, ParameterContainer::TYPE_INTEGER);
+ $this->statement->setParameterContainer($container);
+
+ $result = $this->statement->execute();
+
+ self::assertInstanceOf(Result::class, $result);
+ }
+
+ public function testBindParametersWithErrataTypeNull(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT :val');
+
+ $container = new ParameterContainer();
+ $container->offsetSet('val', null, ParameterContainer::TYPE_NULL);
+ $this->statement->setParameterContainer($container);
+
+ $result = $this->statement->execute();
+
+ self::assertInstanceOf(Result::class, $result);
+ }
+
+ public function testBindParametersWithErrataTypeLob(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT :val');
+
+ $container = new ParameterContainer();
+ $container->offsetSet('val', 'data', ParameterContainer::TYPE_LOB);
+ $this->statement->setParameterContainer($container);
+
+ $result = $this->statement->execute();
+
+ self::assertInstanceOf(Result::class, $result);
+ }
+
+ public function testBindParametersWithErrataTypeDefaultsToString(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT :val');
+
+ $container = new ParameterContainer();
+ $container->offsetSet('val', 'data', ParameterContainer::TYPE_BINARY);
+ $this->statement->setParameterContainer($container);
+
+ $result = $this->statement->execute();
+
+ self::assertInstanceOf(Result::class, $result);
+ }
+
+ public function testBindParametersDetectsNullValueType(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT :val');
+
+ $container = new ParameterContainer();
+ $container->offsetSet('val', null);
+ $this->statement->setParameterContainer($container);
+
+ $result = $this->statement->execute();
+
+ self::assertInstanceOf(Result::class, $result);
+ }
+
+ public function testBindParametersDetectsBooleanValueType(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT :val');
+
+ $container = new ParameterContainer();
+ $container->offsetSet('val', true);
+ $this->statement->setParameterContainer($container);
+
+ $result = $this->statement->execute();
+
+ self::assertInstanceOf(Result::class, $result);
+ }
+
+ public function testCloneWithNullParameterContainerDoesNotClone(): void
+ {
+ $statement = new Statement(null);
+ $clone = clone $statement;
+
+ self::assertFalse($clone->isPrepared());
+ self::assertNull($clone->getResource());
+ }
+
+ public function testExecuteAutoPrepares(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT 1');
+
+ self::assertFalse($this->statement->isPrepared());
+
+ $result = $this->statement->execute();
+
+ self::assertInstanceOf(Result::class, $result);
+ self::assertTrue($this->statement->isPrepared());
+ }
+
+ public function testSecondExecuteSkipsBindingWhenAlreadyBound(): void
+ {
+ $pdo = new SqliteMemoryPdo();
+ $this->statement->setDriver(new TestPdo(new TestConnection($pdo)));
+ $this->statement->initialize($pdo);
+ $this->statement->setSql('SELECT :val');
+ $this->statement->setParameterContainer(new ParameterContainer(['val' => 'test']));
+
+ $result1 = $this->statement->execute();
+ self::assertInstanceOf(Result::class, $result1);
+
+ $result2 = $this->statement->execute();
+ self::assertInstanceOf(Result::class, $result2);
+ }
+
+ public function testCloneClonesParameterContainerWhenSet(): void
+ {
+ $container = new ParameterContainer(['key' => 'value']);
+ $statement = new Statement($container);
+
+ $clone = clone $statement;
+
+ self::assertNotSame($container, $clone->getParameterContainer());
+ self::assertSame('value', $clone->getParameterContainer()->offsetGet('key'));
+ }
}
diff --git a/test/unit/Adapter/Driver/Pdo/TestAsset/TestPdoWithFeatures.php b/test/unit/Adapter/Driver/Pdo/TestAsset/TestPdoWithFeatures.php
new file mode 100644
index 00000000..b4c1164b
--- /dev/null
+++ b/test/unit/Adapter/Driver/Pdo/TestAsset/TestPdoWithFeatures.php
@@ -0,0 +1,53 @@
+resultPrototype;
+ $result->initialize($resource, $this->connection->getLastGeneratedValue());
+ return $result;
+ }
+
+ #[Override]
+ public function getDatabasePlatformName(string $nameFormat = self::NAME_FORMAT_CAMELCASE): string
+ {
+ return 'TestWithFeatures';
+ }
+}
diff --git a/test/unit/Adapter/Driver/TestAsset/TestConnection.php b/test/unit/Adapter/Driver/TestAsset/TestConnection.php
new file mode 100644
index 00000000..bf81ffa2
--- /dev/null
+++ b/test/unit/Adapter/Driver/TestAsset/TestConnection.php
@@ -0,0 +1,56 @@
+resource = 'fake-resource';
+
+ return $this;
+ }
+
+ public function execute(string $sql): ?ResultInterface
+ {
+ return null;
+ }
+
+ public function getCurrentSchema(): string|false
+ {
+ return false;
+ }
+
+ public function getLastGeneratedValue(?string $name = null): string|int|false|null
+ {
+ return false;
+ }
+
+ public function isConnected(): bool
+ {
+ return $this->resource !== null;
+ }
+
+ public function rollback(): ConnectionInterface
+ {
+ return $this;
+ }
+}
diff --git a/test/unit/Adapter/Driver/TestAsset/TestFeatureDriver.php b/test/unit/Adapter/Driver/TestAsset/TestFeatureDriver.php
new file mode 100644
index 00000000..70d47a9c
--- /dev/null
+++ b/test/unit/Adapter/Driver/TestAsset/TestFeatureDriver.php
@@ -0,0 +1,71 @@
+getMessage());
+ }
+}
diff --git a/test/unit/Adapter/ParameterContainerTest.php b/test/unit/Adapter/ParameterContainerTest.php
index 4a7ee509..d80c00d8 100644
--- a/test/unit/Adapter/ParameterContainerTest.php
+++ b/test/unit/Adapter/ParameterContainerTest.php
@@ -5,8 +5,10 @@
namespace PhpDbTest\Adapter;
use Override;
+use PhpDb\Adapter\Exception\InvalidArgumentException;
use PhpDb\Adapter\ParameterContainer;
use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
@@ -32,6 +34,10 @@
#[CoversMethod(ParameterContainer::class, 'key')]
#[CoversMethod(ParameterContainer::class, 'valid')]
#[CoversMethod(ParameterContainer::class, 'rewind')]
+#[CoversMethod(ParameterContainer::class, '__construct')]
+#[CoversMethod(ParameterContainer::class, 'offsetSetReference')]
+#[CoversMethod(ParameterContainer::class, 'getPositionalArray')]
+#[Group('unit')]
final class ParameterContainerTest extends TestCase
{
protected ParameterContainer $parameterContainer;
@@ -264,4 +270,170 @@ public function testRewind(): void
$this->parameterContainer->rewind();
self::assertEquals('foo', $this->parameterContainer->key());
}
+
+ public function testConstructorWithDataPopulatesContainer(): void
+ {
+ $container = new ParameterContainer(['a' => 1, 'b' => 2]);
+
+ self::assertSame(2, $container->count());
+ self::assertSame(1, $container->offsetGet('a'));
+ self::assertSame(2, $container->offsetGet('b'));
+ }
+
+ public function testOffsetSetReferenceCreatesReference(): void
+ {
+ $container = new ParameterContainer(['source' => 'original']);
+ $container->offsetSetReference('alias', 'source');
+
+ self::assertSame('original', $container->offsetGet('alias'));
+ }
+
+ public function testOffsetSetWithIntNotInPositionsCastsToString(): void
+ {
+ $container = new ParameterContainer();
+ $container->offsetSet(5, 'value');
+
+ self::assertSame('value', $container->offsetGet('5'));
+ }
+
+ public function testOffsetSetWithNameMappingMatchForNonColonName(): void
+ {
+ $container = new ParameterContainer();
+ $container->offsetSet('c_0', ':myparam');
+ $container->offsetSet('myparam', 'updated');
+
+ self::assertSame('updated', $container->offsetGet('c_0'));
+ }
+
+ public function testOffsetSetThrowsOnInvalidKeyType(): void
+ {
+ $container = new ParameterContainer();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Keys must be string, integer or null');
+
+ $container->offsetSet(1.5, 'value');
+ }
+
+ public function testOffsetUnsetByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['a' => 'one', 'b' => 'two']);
+ $container->offsetUnset(0);
+
+ self::assertFalse($container->offsetExists('a'));
+ self::assertTrue($container->offsetExists('b'));
+ }
+
+ public function testOffsetSetMaxLengthByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+ $container->offsetSetMaxLength(0, 50);
+
+ self::assertSame(50, $container->offsetGetMaxLength('foo'));
+ }
+
+ public function testOffsetGetMaxLengthByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+ $container->offsetSetMaxLength('foo', 50);
+
+ self::assertSame(50, $container->offsetGetMaxLength(0));
+ }
+
+ public function testOffsetGetMaxLengthThrowsWhenNameDoesNotExist(): void
+ {
+ $container = new ParameterContainer();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Data does not exist for this name/position');
+
+ $container->offsetGetMaxLength('nonexistent');
+ }
+
+ public function testOffsetHasMaxLengthByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+ $container->offsetSetMaxLength('foo', 50);
+
+ self::assertTrue($container->offsetHasMaxLength(0));
+ }
+
+ public function testOffsetUnsetMaxLengthByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+ $container->offsetSetMaxLength('foo', 50);
+ $container->offsetUnsetMaxLength(0);
+
+ self::assertNull($container->offsetGetMaxLength('foo'));
+ }
+
+ public function testOffsetUnsetMaxLengthThrowsWhenNameDoesNotExist(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Data does not exist for this name/position');
+
+ $container->offsetUnsetMaxLength('foo');
+ }
+
+ public function testOffsetSetErrataByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+ $container->offsetSetErrata(0, ParameterContainer::TYPE_STRING);
+
+ self::assertSame(ParameterContainer::TYPE_STRING, $container->offsetGetErrata('foo'));
+ }
+
+ public function testOffsetGetErrataByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+ $container->offsetSetErrata('foo', ParameterContainer::TYPE_INTEGER);
+
+ self::assertSame(ParameterContainer::TYPE_INTEGER, $container->offsetGetErrata(0));
+ }
+
+ public function testOffsetGetErrataThrowsWhenNameDoesNotExist(): void
+ {
+ $container = new ParameterContainer();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Data does not exist for this name/position');
+
+ $container->offsetGetErrata('nonexistent');
+ }
+
+ public function testOffsetHasErrataByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+ $container->offsetSetErrata('foo', ParameterContainer::TYPE_STRING);
+
+ self::assertTrue($container->offsetHasErrata(0));
+ }
+
+ public function testOffsetUnsetErrataByPositionalIndex(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+ $container->offsetSetErrata('foo', ParameterContainer::TYPE_STRING);
+ $container->offsetUnsetErrata(0);
+
+ self::assertNull($container->offsetGetErrata('foo'));
+ }
+
+ public function testOffsetUnsetErrataThrowsWhenNameDoesNotExist(): void
+ {
+ $container = new ParameterContainer(['foo' => 'bar']);
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Data does not exist for this name/position');
+
+ $container->offsetUnsetErrata('foo');
+ }
+
+ public function testGetPositionalArrayReturnsValues(): void
+ {
+ $container = new ParameterContainer(['a' => 1, 'b' => 2, 'c' => 3]);
+
+ self::assertSame([1, 2, 3], $container->getPositionalArray());
+ }
}
diff --git a/test/unit/Adapter/Platform/Sql92Test.php b/test/unit/Adapter/Platform/Sql92Test.php
index ef56ea76..3ebff997 100644
--- a/test/unit/Adapter/Platform/Sql92Test.php
+++ b/test/unit/Adapter/Platform/Sql92Test.php
@@ -5,9 +5,14 @@
namespace PhpDbTest\Adapter\Platform;
use Override;
+use PhpDb\Adapter\Driver\DriverInterface;
use PhpDb\Adapter\Exception\VunerablePlatformQuoteException;
+use PhpDb\Adapter\Platform\AbstractPlatform;
use PhpDb\Adapter\Platform\Sql92;
+use PhpDbTest\Adapter\Platform\TestAsset\TestPlatform;
+use PhpDbTest\TestAsset\TestSql92Platform;
use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[CoversMethod(Sql92::class, 'getName')]
@@ -20,6 +25,16 @@
#[CoversMethod(Sql92::class, 'quoteValueList')]
#[CoversMethod(Sql92::class, 'getIdentifierSeparator')]
#[CoversMethod(Sql92::class, 'quoteIdentifierInFragment')]
+#[CoversMethod(AbstractPlatform::class, 'quoteIdentifier')]
+#[CoversMethod(AbstractPlatform::class, 'quoteIdentifierInFragment')]
+#[CoversMethod(AbstractPlatform::class, 'quoteValue')]
+#[CoversMethod(AbstractPlatform::class, 'quoteIdentifierChain')]
+#[CoversMethod(AbstractPlatform::class, 'getQuoteIdentifierSymbol')]
+#[CoversMethod(AbstractPlatform::class, 'getQuoteValueSymbol')]
+#[CoversMethod(AbstractPlatform::class, 'quoteTrustedValue')]
+#[CoversMethod(AbstractPlatform::class, 'quoteValueList')]
+#[CoversMethod(AbstractPlatform::class, 'getIdentifierSeparator')]
+#[Group('unit')]
final class Sql92Test extends TestCase
{
protected Sql92 $platform;
@@ -138,4 +153,44 @@ public function testQuoteIdentifierInFragment(): void
)
);
}
+
+ public function testQuoteIdentifierReturnsUnquotedWhenQuotingDisabled(): void
+ {
+ $platform = new TestSql92Platform(quoteIdentifiers: false);
+
+ self::assertSame('test', $platform->quoteIdentifier('test'));
+ }
+
+ public function testQuoteIdentifierInFragmentReturnsUnquotedWhenQuotingDisabled(): void
+ {
+ $platform = new TestSql92Platform(quoteIdentifiers: false);
+
+ self::assertSame('foo.bar', $platform->quoteIdentifierInFragment('foo.bar'));
+ }
+
+ public function testQuoteValueEscapesSpecialCharacters(): void
+ {
+ $platform = new TestSql92Platform(driver: $this->createStub(DriverInterface::class));
+
+ $quoted = $platform->quoteValue("test'value");
+
+ self::assertStringContainsString('test', $quoted);
+ self::assertStringStartsWith("'", $quoted);
+ self::assertStringEndsWith("'", $quoted);
+ }
+
+ public function testAbstractPlatformQuoteValueThrowsWithoutDriver(): void
+ {
+ $platform = new TestPlatform();
+
+ $this->expectException(VunerablePlatformQuoteException::class);
+ $platform->quoteValue('value');
+ }
+
+ public function testAbstractPlatformQuoteValueEscapesWithDriver(): void
+ {
+ $platform = new TestPlatform($this->createStub(DriverInterface::class));
+
+ self::assertSame("'test\\'value'", $platform->quoteValue("test'value"));
+ }
}
diff --git a/test/unit/Adapter/Platform/TestAsset/TestPlatform.php b/test/unit/Adapter/Platform/TestAsset/TestPlatform.php
new file mode 100644
index 00000000..9b87d9d5
--- /dev/null
+++ b/test/unit/Adapter/Platform/TestAsset/TestPlatform.php
@@ -0,0 +1,31 @@
+profiler = new Profiler();
}
- public function testProfilerStart(): void
+ public function testProfilerStartWithString(): void
{
$ret = $this->profiler->profilerStart('SELECT * FROM FOO');
self::assertSame($this->profiler, $ret);
+ }
+
+ public function testProfilerStartWithStatementContainer(): void
+ {
$ret = $this->profiler->profilerStart(new StatementContainer());
self::assertSame($this->profiler, $ret);
-
- $this->expectException(TypeError::class);
- $this->profiler->profilerStart(5);
}
public function testProfilerFinish(): void
@@ -74,4 +77,19 @@ public function testGetProfiles(): void
self::assertCount(2, $this->profiler->getProfiles());
}
+
+ public function testProfilerStartClonesParameterContainerFromStatementContainer(): void
+ {
+ $parameterContainer = new ParameterContainer(['key' => 'value']);
+ $statementContainer = new StatementContainer('SELECT ?', $parameterContainer);
+
+ $this->profiler->profilerStart($statementContainer);
+ $this->profiler->profilerFinish();
+
+ $profile = $this->profiler->getLastProfile();
+
+ self::assertSame('SELECT ?', $profile['sql']);
+ self::assertInstanceOf(ParameterContainer::class, $profile['parameters']);
+ self::assertNotSame($parameterContainer, $profile['parameters']);
+ }
}
diff --git a/test/unit/Adapter/StatementContainerTest.php b/test/unit/Adapter/StatementContainerTest.php
new file mode 100644
index 00000000..42a4767e
--- /dev/null
+++ b/test/unit/Adapter/StatementContainerTest.php
@@ -0,0 +1,55 @@
+getSql());
+ }
+
+ public function testConstructorWithoutSqlDoesNotSetSql(): void
+ {
+ $container = new StatementContainer();
+
+ self::assertSame('', $container->getSql());
+ }
+
+ public function testSetAndGetSql(): void
+ {
+ $container = new StatementContainer();
+
+ $result = $container->setSql('test');
+
+ self::assertSame($container, $result);
+ self::assertSame('test', $container->getSql());
+ }
+
+ public function testSetAndGetParameterContainer(): void
+ {
+ $container = new StatementContainer();
+ $parameterContainer = new ParameterContainer(['a' => 1]);
+
+ $result = $container->setParameterContainer($parameterContainer);
+
+ self::assertSame($container, $result);
+ self::assertSame($parameterContainer, $container->getParameterContainer());
+ }
+}
diff --git a/test/unit/Container/AdapterInterfaceFactoryTest.php b/test/unit/Container/AdapterInterfaceFactoryTest.php
new file mode 100644
index 00000000..dcdab41f
--- /dev/null
+++ b/test/unit/Container/AdapterInterfaceFactoryTest.php
@@ -0,0 +1,137 @@
+createMock(DriverInterface::class);
+ $platformMock = $this->createMock(PlatformInterface::class);
+
+ $container = new ServiceManager([
+ 'factories' => [
+ DriverInterface::class => static fn() => $driverMock,
+ PlatformInterface::class => static fn() => $platformMock,
+ ],
+ ]);
+
+ $factory = new AdapterInterfaceFactory();
+
+ $this->expectException(ContainerException::class);
+ $this->expectExceptionMessage('Container is missing a config service');
+ $factory($container, AdapterInterface::class);
+ }
+
+ public function testInvokeThrowsWhenAdapterConfigIsEmpty(): void
+ {
+ $driverMock = $this->createMock(DriverInterface::class);
+ $platformMock = $this->createMock(PlatformInterface::class);
+
+ $container = new ServiceManager([
+ 'factories' => [
+ DriverInterface::class => static fn() => $driverMock,
+ PlatformInterface::class => static fn() => $platformMock,
+ ],
+ ]);
+ $container->setService('config', []);
+
+ $factory = new AdapterInterfaceFactory();
+
+ $this->expectException(ContainerException::class);
+ $this->expectExceptionMessage('No configuration found for');
+ $factory($container, AdapterInterface::class);
+ }
+
+ public function testInvokeCreatesAdapterWithAllDependencies(): void
+ {
+ $driverMock = $this->createMock(DriverInterface::class);
+ $platformMock = $this->createMock(PlatformInterface::class);
+ $profilerMock = $this->createMock(ProfilerInterface::class);
+ $resultSet = new ResultSet();
+
+ $container = new ServiceManager([
+ 'factories' => [
+ DriverInterface::class => static fn() => $driverMock,
+ PlatformInterface::class => static fn() => $platformMock,
+ ProfilerInterface::class => static fn() => $profilerMock,
+ ],
+ ]);
+ $container->setService('config', [
+ AdapterInterface::class => [
+ 'driver' => DriverInterface::class,
+ ],
+ ]);
+ $container->setService(ResultSetInterface::class, $resultSet);
+
+ $factory = new AdapterInterfaceFactory();
+ $adapter = $factory($container, AdapterInterface::class);
+
+ self::assertInstanceOf(Adapter::class, $adapter);
+ }
+
+ public function testInvokeCreatesAdapterWithoutOptionalProfiler(): void
+ {
+ $driverMock = $this->createMock(DriverInterface::class);
+ $platformMock = $this->createMock(PlatformInterface::class);
+
+ $container = new ServiceManager([
+ 'factories' => [
+ DriverInterface::class => static fn() => $driverMock,
+ PlatformInterface::class => static fn() => $platformMock,
+ ],
+ ]);
+ $container->setService('config', [
+ AdapterInterface::class => [
+ 'driver' => DriverInterface::class,
+ ],
+ ]);
+
+ $factory = new AdapterInterfaceFactory();
+ $adapter = $factory($container, AdapterInterface::class);
+
+ self::assertInstanceOf(Adapter::class, $adapter);
+ self::assertNull($adapter->getProfiler());
+ }
+
+ public function testInvokeCreatesAdapterWithDefaultResultSet(): void
+ {
+ $driverMock = $this->createMock(DriverInterface::class);
+ $platformMock = $this->createMock(PlatformInterface::class);
+
+ $container = new ServiceManager([
+ 'factories' => [
+ DriverInterface::class => static fn() => $driverMock,
+ PlatformInterface::class => static fn() => $platformMock,
+ ],
+ ]);
+ $container->setService('config', [
+ AdapterInterface::class => [
+ 'driver' => DriverInterface::class,
+ ],
+ ]);
+
+ $factory = new AdapterInterfaceFactory();
+ $adapter = $factory($container, AdapterInterface::class);
+
+ self::assertInstanceOf(ResultSet::class, $adapter->getQueryResultSetPrototype());
+ }
+}
diff --git a/test/unit/Exception/ContainerExceptionTest.php b/test/unit/Exception/ContainerExceptionTest.php
new file mode 100644
index 00000000..5356edcc
--- /dev/null
+++ b/test/unit/Exception/ContainerExceptionTest.php
@@ -0,0 +1,32 @@
+getMessage());
+ self::assertStringContainsString('Factory', $exception->getMessage());
+ self::assertStringContainsString('reason', $exception->getMessage());
+ }
+
+ public function testImplementsContainerExceptionInterface(): void
+ {
+ $exception = ContainerException::forService('Svc', 'Factory', 'reason');
+
+ self::assertInstanceOf(ContainerExceptionInterface::class, $exception);
+ }
+}
diff --git a/test/unit/Metadata/Object/AbstractTableObjectTest.php b/test/unit/Metadata/Object/AbstractTableObjectTest.php
index f6e41884..93644eb0 100644
--- a/test/unit/Metadata/Object/AbstractTableObjectTest.php
+++ b/test/unit/Metadata/Object/AbstractTableObjectTest.php
@@ -7,14 +7,14 @@
use PhpDb\Metadata\Object\AbstractTableObject;
use PhpDb\Metadata\Object\ColumnObject;
use PhpDb\Metadata\Object\ConstraintObject;
+use PhpDbTest\Metadata\Object\TestAsset\ConcreteTableObject;
use PHPUnit\Framework\TestCase;
final class AbstractTableObjectTest extends TestCase
{
private function createConcreteTableObject(?string $name): AbstractTableObject
{
- return new class ($name) extends AbstractTableObject {
- };
+ return new ConcreteTableObject($name);
}
public function testConstructorWithName(): void
diff --git a/test/unit/Metadata/Object/TestAsset/ConcreteTableObject.php b/test/unit/Metadata/Object/TestAsset/ConcreteTableObject.php
new file mode 100644
index 00000000..6641cc19
--- /dev/null
+++ b/test/unit/Metadata/Object/TestAsset/ConcreteTableObject.php
@@ -0,0 +1,11 @@
+abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'table_names' => [
+ 'def' => [
+ 'users' => ['table_type' => 'BASE TABLE'],
+ ],
+ ],
+ 'columns' => [
+ 'def' => [
+ 'users' => [],
+ ],
+ ],
+ 'constraints' => [
+ 'def' => [
+ 'users' => [],
+ ],
+ ],
+ ]);
+
+ $tables = $this->abstractSourceMock->getTables(null);
+
+ self::assertCount(1, $tables);
+ self::assertInstanceOf(TableObject::class, $tables[0]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetTableUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'table_names' => [
+ 'def' => [
+ 'users' => ['table_type' => 'BASE TABLE'],
+ ],
+ ],
+ 'columns' => [
+ 'def' => [
+ 'users' => [],
+ ],
+ ],
+ 'constraints' => [
+ 'def' => [
+ 'users' => [],
+ ],
+ ],
+ ]);
+
+ $table = $this->abstractSourceMock->getTable('users', null);
+
+ self::assertInstanceOf(TableObject::class, $table);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetViewNamesUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'table_names' => [
+ 'def' => [
+ 'v1' => ['table_type' => 'VIEW'],
+ ],
+ ],
+ ]);
+
+ $names = $this->abstractSourceMock->getViewNames(null);
+
+ self::assertSame(['v1'], $names);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetViewsUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'table_names' => [
+ 'def' => [
+ 'v1' => [
+ 'table_type' => 'VIEW',
+ 'view_definition' => 'SELECT 1',
+ 'check_option' => null,
+ 'is_updatable' => true,
+ ],
+ ],
+ ],
+ 'columns' => ['def' => ['v1' => []]],
+ 'constraints' => ['def' => ['v1' => []]],
+ ]);
+
+ $views = $this->abstractSourceMock->getViews(null);
+
+ self::assertCount(1, $views);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetViewUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'table_names' => [
+ 'def' => [
+ 'v1' => [
+ 'table_type' => 'VIEW',
+ 'view_definition' => 'SELECT 1',
+ 'check_option' => null,
+ 'is_updatable' => false,
+ ],
+ ],
+ ],
+ 'columns' => ['def' => ['v1' => []]],
+ 'constraints' => ['def' => ['v1' => []]],
+ ]);
+
+ $view = $this->abstractSourceMock->getView('v1', null);
+
+ self::assertInstanceOf(ViewObject::class, $view);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetColumnNamesUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'columns' => [
+ 'def' => [
+ 'users' => [
+ 'id' => [],
+ 'name' => [],
+ ],
+ ],
+ ],
+ ]);
+
+ $names = $this->abstractSourceMock->getColumnNames('users', null);
+
+ self::assertSame(['id', 'name'], $names);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetColumnsUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'columns' => [
+ 'def' => [
+ 'users' => [
+ 'id' => [
+ 'ordinal_position' => 1,
+ 'column_default' => null,
+ 'is_nullable' => false,
+ 'data_type' => 'INT',
+ 'character_maximum_length' => null,
+ 'character_octet_length' => null,
+ 'numeric_precision' => null,
+ 'numeric_scale' => null,
+ 'numeric_unsigned' => null,
+ 'erratas' => [],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $columns = $this->abstractSourceMock->getColumns('users', null);
+
+ self::assertCount(1, $columns);
+ self::assertInstanceOf(ColumnObject::class, $columns[0]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetColumnUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'columns' => [
+ 'def' => [
+ 'users' => [
+ 'id' => [
+ 'ordinal_position' => 1,
+ 'column_default' => null,
+ 'is_nullable' => false,
+ 'data_type' => 'INT',
+ 'character_maximum_length' => null,
+ 'character_octet_length' => null,
+ 'numeric_precision' => null,
+ 'numeric_scale' => null,
+ 'numeric_unsigned' => null,
+ 'erratas' => [],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $column = $this->abstractSourceMock->getColumn('id', 'users', null);
+
+ self::assertInstanceOf(ColumnObject::class, $column);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetConstraintsUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'constraints' => [
+ 'def' => [
+ 'users' => [
+ 'pk' => [
+ 'constraint_type' => 'PRIMARY KEY',
+ 'columns' => ['id'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $constraints = $this->abstractSourceMock->getConstraints('users', null);
+
+ self::assertCount(1, $constraints);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetConstraintUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'constraints' => [
+ 'def' => [
+ 'users' => [
+ 'pk' => [
+ 'constraint_type' => 'PRIMARY KEY',
+ 'columns' => ['id'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $constraint = $this->abstractSourceMock->getConstraint('pk', 'users', null);
+
+ self::assertInstanceOf(ConstraintObject::class, $constraint);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetConstraintKeysUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'constraint_references' => [
+ 'def' => [],
+ ],
+ 'constraint_keys' => [
+ 'def' => [
+ [
+ 'table_name' => 'users',
+ 'constraint_name' => 'pk',
+ 'column_name' => 'id',
+ 'ordinal_position' => 1,
+ ],
+ ],
+ ],
+ ]);
+
+ $keys = $this->abstractSourceMock->getConstraintKeys('pk', 'users', null);
+
+ self::assertCount(1, $keys);
+ self::assertInstanceOf(ConstraintKeyObject::class, $keys[0]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetTriggerNamesUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'triggers' => [
+ 'def' => [
+ 'trig1' => [],
+ ],
+ ],
+ ]);
+
+ $names = $this->abstractSourceMock->getTriggerNames(null);
+
+ self::assertSame(['trig1'], $names);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetTriggersUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'triggers' => [
+ 'def' => [
+ 'trig1' => [
+ 'event_manipulation' => 'INSERT',
+ 'event_object_catalog' => 'cat',
+ 'event_object_schema' => 'def',
+ 'event_object_table' => 'users',
+ 'action_order' => '1',
+ 'action_condition' => null,
+ 'action_statement' => 'BEGIN END',
+ 'action_orientation' => 'ROW',
+ 'action_timing' => 'BEFORE',
+ 'action_reference_old_table' => null,
+ 'action_reference_new_table' => null,
+ 'action_reference_old_row' => 'OLD',
+ 'action_reference_new_row' => 'NEW',
+ 'created' => null,
+ ],
+ ],
+ ],
+ ]);
+
+ $triggers = $this->abstractSourceMock->getTriggers(null);
+
+ self::assertCount(1, $triggers);
+ self::assertInstanceOf(TriggerObject::class, $triggers[0]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testGetTriggerUsesDefaultSchemaWhenNull(): void
+ {
+ $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema');
+ $refProp->setValue($this->abstractSourceMock, 'def');
+
+ $this->setMockData([
+ 'triggers' => [
+ 'def' => [
+ 'trig1' => [
+ 'event_manipulation' => 'INSERT',
+ 'event_object_catalog' => 'cat',
+ 'event_object_schema' => 'def',
+ 'event_object_table' => 'users',
+ 'action_order' => '1',
+ 'action_condition' => null,
+ 'action_statement' => 'BEGIN END',
+ 'action_orientation' => 'ROW',
+ 'action_timing' => 'BEFORE',
+ 'action_reference_old_table' => null,
+ 'action_reference_new_table' => null,
+ 'action_reference_old_row' => 'OLD',
+ 'action_reference_new_row' => 'NEW',
+ 'created' => null,
+ ],
+ ],
+ ],
+ ]);
+
+ $trigger = $this->abstractSourceMock->getTrigger('trig1', null);
+
+ self::assertInstanceOf(TriggerObject::class, $trigger);
+ self::assertSame('trig1', $trigger->getName());
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testLoadTableNameDataCallsPrepareDataHierarchy(): void
+ {
+ $method = new ReflectionMethod($this->abstractSourceMock, 'loadTableNameData');
+ $method->invoke($this->abstractSourceMock, 'test_schema');
+
+ $data = $this->getMockData();
+ self::assertArrayHasKey('table_names', $data);
+ self::assertArrayHasKey('test_schema', $data['table_names']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testLoadColumnDataCallsPrepareDataHierarchy(): void
+ {
+ $method = new ReflectionMethod($this->abstractSourceMock, 'loadColumnData');
+ $method->invoke($this->abstractSourceMock, 'users', 'test_schema');
+
+ $data = $this->getMockData();
+ self::assertArrayHasKey('columns', $data);
+ self::assertArrayHasKey('test_schema', $data['columns']);
+ self::assertArrayHasKey('users', $data['columns']['test_schema']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testLoadConstraintDataCallsPrepareDataHierarchy(): void
+ {
+ $method = new ReflectionMethod($this->abstractSourceMock, 'loadConstraintData');
+ $method->invoke($this->abstractSourceMock, 'users', 'test_schema');
+
+ $data = $this->getMockData();
+ self::assertArrayHasKey('constraints', $data);
+ self::assertArrayHasKey('test_schema', $data['constraints']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testLoadConstraintDataKeysCallsPrepareDataHierarchy(): void
+ {
+ $method = new ReflectionMethod($this->abstractSourceMock, 'loadConstraintDataKeys');
+ $method->invoke($this->abstractSourceMock, 'test_schema');
+
+ $data = $this->getMockData();
+ self::assertArrayHasKey('constraint_keys', $data);
+ self::assertArrayHasKey('test_schema', $data['constraint_keys']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testLoadConstraintReferencesCallsPrepareDataHierarchy(): void
+ {
+ $method = new ReflectionMethod($this->abstractSourceMock, 'loadConstraintReferences');
+ $method->invoke($this->abstractSourceMock, 'users', 'test_schema');
+
+ $data = $this->getMockData();
+ self::assertArrayHasKey('constraint_references', $data);
+ self::assertArrayHasKey('test_schema', $data['constraint_references']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testLoadTriggerDataCallsPrepareDataHierarchy(): void
+ {
+ $method = new ReflectionMethod($this->abstractSourceMock, 'loadTriggerData');
+ $method->invoke($this->abstractSourceMock, 'test_schema');
+
+ $data = $this->getMockData();
+ self::assertArrayHasKey('triggers', $data);
+ self::assertArrayHasKey('test_schema', $data['triggers']);
+ }
+
+ public function testGetColumnNamesThrowsWhenLoadColumnDataDoesNotPopulate(): void
+ {
+ $adapter = $this->createMockForIntersectionOfInterfaces([
+ AdapterInterface::class,
+ SchemaAwareInterface::class,
+ ]);
+ $adapter->method('getCurrentSchema')->willReturn('public');
+
+ $source = new IncompleteSource($adapter);
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('"nonexistent" does not exist');
+ $source->getColumnNames('nonexistent', 'public');
+ }
}
diff --git a/test/unit/Metadata/Source/TestAsset/IncompleteSource.php b/test/unit/Metadata/Source/TestAsset/IncompleteSource.php
new file mode 100644
index 00000000..9d856bd3
--- /dev/null
+++ b/test/unit/Metadata/Source/TestAsset/IncompleteSource.php
@@ -0,0 +1,22 @@
+createResultSetMock();
+ $resultSet->initialize(new ArrayIterator([
+ ['id' => 1, 'name' => 'one'],
+ ['id' => 2, 'name' => 'two'],
+ ]));
+ $resultSet->buffer();
+
+ $firstPass = [];
+ foreach ($resultSet as $row) {
+ $firstPass[] = $row;
+ }
+
+ $resultSet->rewind();
+
+ $secondPass = [];
+ foreach ($resultSet as $row) {
+ $secondPass[] = $row;
+ }
+
+ self::assertEquals($firstPass, $secondPass);
+ }
+
+ public function testToArrayConvertsArrayObjectsViaGetArrayCopy(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize([
+ new ArrayObject(['id' => 1, 'name' => 'one']),
+ ]);
+
+ $result = $resultSet->toArray();
+
+ self::assertSame([['id' => 1, 'name' => 'one']], $result);
+ }
+
+ public function testGetFieldCountReturnsZeroForEmptyIterator(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize(new ArrayIterator([]));
+
+ self::assertSame(0, $resultSet->getFieldCount());
+ }
+
+ public function testInitializeWithBufferedResultInterface(): void
+ {
+ $result = $this->createMock(ResultInterface::class);
+ $result->method('isBuffered')->willReturn(true);
+ $result->method('getFieldCount')->willReturn(2);
+
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize($result);
+
+ self::assertTrue($resultSet->isBuffered());
+ }
+
+ public function testCountReturnsNullForUncountableDataSource(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $iterator = new NoRewindIterator(new ArrayIterator([['id' => 1]]));
+ $resultSet->initialize($iterator);
+
+ self::assertNull($resultSet->count());
+ }
+
+ public function testValidReturnsFalseAfterLastElement(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize(new ArrayIterator([
+ ['id' => 1],
+ ]));
+
+ self::assertTrue($resultSet->valid());
+ $resultSet->next();
+ self::assertFalse($resultSet->valid());
+ }
+
/**
* Test multiple iterations with buffer
*
@@ -378,4 +459,135 @@ public function testMultipleRewindBufferIterations(): void
$data = $resultSet->current();
self::assertEquals(3, $data['id']);
}
+
+ /**
+ * @throws Exception
+ */
+ public function testInitializeResetsBufferWhenAlreadyBuffered(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize(new ArrayIterator([['id' => 1]]));
+ $resultSet->buffer();
+
+ $resultSet->initialize(new ArrayIterator([['id' => 2]]));
+
+ self::assertSame(2, $resultSet->current()['id']);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testInitializeWithResultInterfaceRewindsWhenBuffered(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize(new ArrayIterator([['id' => 1]]));
+ $resultSet->buffer();
+
+ $result = $this->createMock(ResultInterface::class);
+ $result->method('getFieldCount')->willReturn(2);
+ $result->method('isBuffered')->willReturn(false);
+ $result->expects(self::once())->method('rewind');
+
+ $resultSet->initialize($result);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testInitializeWithIteratorAggregate(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $aggregate = new class implements IteratorAggregate {
+ public function getIterator(): ArrayIterator
+ {
+ return new ArrayIterator([['id' => 1], ['id' => 2]]);
+ }
+ };
+
+ $resultSet->initialize($aggregate);
+
+ self::assertSame(1, $resultSet->current()['id']);
+ }
+
+ public function testGetFieldCountReturnsCachedValue(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize([['a' => 1, 'b' => 2]]);
+
+ $first = $resultSet->getFieldCount();
+ $second = $resultSet->getFieldCount();
+
+ self::assertSame(2, $first);
+ self::assertSame($first, $second);
+ }
+
+ public function testGetFieldCountReturnsZeroWithNoDataSource(): void
+ {
+ $resultSet = $this->createResultSetMock();
+
+ self::assertSame(0, $resultSet->getFieldCount());
+ }
+
+ public function testGetFieldCountWithCountableRow(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize(new ArrayIterator([new ArrayObject(['a' => 1, 'b' => 2, 'c' => 3])]));
+
+ self::assertSame(3, $resultSet->getFieldCount());
+ }
+
+ public function testValidWithNonIteratorDataSource(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $aggregate = new class implements IteratorAggregate {
+ public function getIterator(): ArrayIterator
+ {
+ return new ArrayIterator([['id' => 1]]);
+ }
+ };
+
+ $resultSet->initialize($aggregate);
+ $resultSet->rewind();
+
+ self::assertTrue($resultSet->valid());
+ }
+
+ public function testRewindWithNonIteratorDataSource(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $aggregate = new class implements IteratorAggregate {
+ public function getIterator(): ArrayIterator
+ {
+ return new ArrayIterator([['id' => 1], ['id' => 2]]);
+ }
+ };
+
+ $resultSet->initialize($aggregate);
+ $resultSet->next();
+ $resultSet->rewind();
+
+ self::assertSame(0, $resultSet->key());
+ }
+
+ public function testCountReturnsCachedResult(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize([['id' => 1], ['id' => 2]]);
+
+ $first = $resultSet->count();
+ $second = $resultSet->count();
+
+ self::assertSame(2, $first);
+ self::assertSame($first, $second);
+ }
+
+ public function testToArrayThrowsOnNonCastableRows(): void
+ {
+ $resultSet = $this->createResultSetMock();
+ $resultSet->initialize(new ArrayIterator([new stdClass()]));
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('cannot be cast to an array');
+ $resultSet->toArray();
+ }
}
diff --git a/test/unit/ResultSet/HydratingResultSetTest.php b/test/unit/ResultSet/HydratingResultSetTest.php
index f3f1c792..dc63d268 100644
--- a/test/unit/ResultSet/HydratingResultSetTest.php
+++ b/test/unit/ResultSet/HydratingResultSetTest.php
@@ -4,12 +4,16 @@
namespace PhpDbTest\ResultSet;
+use ArrayIterator;
+use ArrayObject;
use Exception;
use Laminas\Hydrator\ArraySerializableHydrator;
use Laminas\Hydrator\ClassMethodsHydrator;
use Override;
+use PhpDb\ResultSet\Exception\RuntimeException;
use PhpDb\ResultSet\HydratingResultSet;
use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use stdClass;
@@ -19,6 +23,10 @@
#[CoversMethod(HydratingResultSet::class, 'getHydrator')]
#[CoversMethod(HydratingResultSet::class, 'current')]
#[CoversMethod(HydratingResultSet::class, 'toArray')]
+#[CoversMethod(HydratingResultSet::class, '__construct')]
+#[CoversMethod(HydratingResultSet::class, 'setRowPrototype')]
+#[CoversMethod(HydratingResultSet::class, 'getRowPrototype')]
+#[Group('unit')]
final class HydratingResultSetTest extends TestCase
{
private string $arraySerializableHydratorClass;
@@ -138,4 +146,72 @@ public function testToArray(): void
$obj = $hydratingRs->toArray();
self::assertIsArray($obj);
}
+
+ public function testConstructorDefaultsToArraySerializableHydrator(): void
+ {
+ $hydratingRs = new HydratingResultSet();
+
+ self::assertInstanceOf(ArraySerializableHydrator::class, $hydratingRs->getHydrator());
+ }
+
+ public function testSetRowPrototypeStoresPrototype(): void
+ {
+ $hydratingRs = new HydratingResultSet();
+ $prototype = new stdClass();
+
+ $result = $hydratingRs->setRowPrototype($prototype);
+
+ self::assertSame($hydratingRs, $result);
+ self::assertSame($prototype, $hydratingRs->getRowPrototype());
+ }
+
+ public function testGetRowPrototypeReturnsDefaultArrayObject(): void
+ {
+ $hydratingRs = new HydratingResultSet();
+
+ self::assertInstanceOf(ArrayObject::class, $hydratingRs->getRowPrototype());
+ }
+
+ public function testCurrentWithBufferReturnsBufferedObject(): void
+ {
+ $hydratingRs = new HydratingResultSet();
+ $hydratingRs->initialize(new ArrayIterator([
+ ['id' => 1, 'name' => 'one'],
+ ['id' => 2, 'name' => 'two'],
+ ]));
+ $hydratingRs->buffer();
+
+ $first = $hydratingRs->current();
+ $hydratingRs->rewind();
+ $buffered = $hydratingRs->current();
+
+ self::assertSame($first, $buffered);
+ }
+
+ public function testToArrayUsesHydratorExtract(): void
+ {
+ $hydratingRs = new HydratingResultSet();
+ $hydratingRs->initialize([
+ ['id' => 1, 'name' => 'one'],
+ ]);
+
+ $result = $hydratingRs->toArray();
+
+ self::assertCount(1, $result);
+ self::assertArrayHasKey('id', $result[0]);
+ self::assertSame(1, $result[0]['id']);
+ }
+
+ public function testCurrentDisablesBufferingImplicitly(): void
+ {
+ $hydratingRs = new HydratingResultSet();
+ $hydratingRs->initialize(new ArrayIterator([
+ ['id' => 1],
+ ]));
+
+ $hydratingRs->current();
+
+ $this->expectException(RuntimeException::class);
+ $hydratingRs->buffer();
+ }
}
diff --git a/test/unit/ResultSet/ResultSetIntegrationTest.php b/test/unit/ResultSet/ResultSetIntegrationTest.php
index 4c417018..4987fb20 100644
--- a/test/unit/ResultSet/ResultSetIntegrationTest.php
+++ b/test/unit/ResultSet/ResultSetIntegrationTest.php
@@ -14,6 +14,7 @@
use PhpDb\ResultSet\ResultSetReturnType;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Random\RandomException;
@@ -27,6 +28,11 @@
#[CoversMethod(AbstractResultSet::class, 'current')]
#[CoversMethod(AbstractResultSet::class, 'buffer')]
+#[CoversMethod(ResultSet::class, 'current')]
+#[CoversMethod(ResultSet::class, 'getReturnType')]
+#[CoversMethod(ResultSet::class, '__construct')]
+#[CoversMethod(ResultSet::class, 'getArrayObjectPrototype')]
+#[Group('unit')]
final class ResultSetIntegrationTest extends TestCase
{
protected ResultSet $resultSet;
@@ -320,4 +326,56 @@ public function testCurrentReturnsNullForNonExistingValues(): void
// Verify current() returns null when data source returns non-array value
self::assertNull($this->resultSet->current());
}
+
+ public function testCurrentReturnsArrayObjectWhenReturnTypeIsArrayObject(): void
+ {
+ $resultSet = new ResultSet(ResultSetReturnType::ArrayObject);
+ $resultSet->initialize([['id' => 1, 'name' => 'one']]);
+
+ $current = $resultSet->current();
+
+ self::assertInstanceOf(ArrayObject::class, $current);
+ self::assertSame(1, $current['id']);
+ }
+
+ public function testCurrentReturnsArrayWhenReturnTypeIsArray(): void
+ {
+ $resultSet = new ResultSet(ResultSetReturnType::Array);
+ $resultSet->initialize([['id' => 1, 'name' => 'one']]);
+
+ $current = $resultSet->current();
+
+ self::assertIsArray($current);
+ self::assertSame(1, $current['id']);
+ }
+
+ public function testCurrentClonesRowPrototypeOnEachCall(): void
+ {
+ $resultSet = new ResultSet(ResultSetReturnType::ArrayObject);
+ $resultSet->initialize([
+ ['id' => 1, 'name' => 'one'],
+ ['id' => 2, 'name' => 'two'],
+ ]);
+
+ $first = $resultSet->current();
+ $resultSet->next();
+ $second = $resultSet->current();
+
+ self::assertNotSame($first, $second);
+ }
+
+ public function testGetReturnTypeReturnsArrayWhenSetToArray(): void
+ {
+ $resultSet = new ResultSet(ResultSetReturnType::Array);
+
+ self::assertSame(ResultSetReturnType::Array, $resultSet->getReturnType());
+ }
+
+ public function testGetArrayObjectPrototypeDelegatesToGetRowPrototype(): void
+ {
+ self::assertSame(
+ $this->resultSet->getRowPrototype(),
+ $this->resultSet->getArrayObjectPrototype()
+ );
+ }
}
diff --git a/test/unit/RowGateway/Feature/FeatureSetTest.php b/test/unit/RowGateway/Feature/FeatureSetTest.php
index b59907b4..5507a126 100644
--- a/test/unit/RowGateway/Feature/FeatureSetTest.php
+++ b/test/unit/RowGateway/Feature/FeatureSetTest.php
@@ -7,6 +7,7 @@
use PhpDb\RowGateway\AbstractRowGateway;
use PhpDb\RowGateway\Feature\AbstractFeature;
use PhpDb\RowGateway\Feature\FeatureSet;
+use PhpDbTest\RowGateway\Feature\TestAsset\TestRowGatewayFeature;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -104,76 +105,27 @@ public function testAddFeatureCallsSetRowGatewayWhenRowGatewayIsSet(): void
public function testApplyCallsMethodOnFeatures(): void
{
- $called = false;
- $receivedArgs = [];
-
- $feature = new class ($called, $receivedArgs) extends AbstractFeature {
- /** @var bool @phpstan-ignore property.onlyWritten */
- private $called;
- /** @var array @phpstan-ignore property.onlyWritten */
- private $receivedArgs;
-
- public function __construct(bool &$called, array &$receivedArgs)
- {
- $this->called = &$called;
- $this->receivedArgs = &$receivedArgs;
- }
-
- public function preInitialize(string $arg1, string $arg2): void
- {
- $this->called = true;
- $this->receivedArgs = [$arg1, $arg2];
- }
- };
+ $feature = new TestRowGatewayFeature();
$featureSet = new FeatureSet([$feature]);
$featureSet->apply('preInitialize', ['arg1', 'arg2']);
- self::assertTrue($called);
- self::assertEquals(['arg1', 'arg2'], $receivedArgs);
+ self::assertTrue($feature->called);
+ self::assertEquals(['arg1', 'arg2'], $feature->receivedArgs);
}
public function testApplyHaltsWhenFeatureReturnsHalt(): void
{
- $feature1Called = false;
- $feature2Called = false;
-
- $feature1 = new class ($feature1Called) extends AbstractFeature {
- /** @var bool @phpstan-ignore property.onlyWritten */
- private $called;
-
- public function __construct(bool &$called)
- {
- $this->called = &$called;
- }
-
- public function preInitialize(): string
- {
- $this->called = true;
- return FeatureSet::APPLY_HALT;
- }
- };
-
- $feature2 = new class ($feature2Called) extends AbstractFeature {
- /** @var bool @phpstan-ignore property.onlyWritten */
- private $called;
-
- public function __construct(bool &$called)
- {
- $this->called = &$called;
- }
+ $feature1 = new TestRowGatewayFeature();
+ $feature1->returnValue = FeatureSet::APPLY_HALT;
- public function preInitialize(): void
- {
- $this->called = true;
- }
- };
+ $feature2 = new TestRowGatewayFeature();
$featureSet = new FeatureSet([$feature1, $feature2]);
$featureSet->apply('preInitialize', []);
- self::assertTrue($feature1Called);
- self::assertFalse($feature2Called);
+ self::assertTrue($feature1->called);
+ self::assertFalse($feature2->called);
}
public function testApplySkipsFeatureWithoutMethod(): void
diff --git a/test/unit/RowGateway/Feature/TestAsset/TestRowGatewayFeature.php b/test/unit/RowGateway/Feature/TestAsset/TestRowGatewayFeature.php
new file mode 100644
index 00000000..6a41fc2d
--- /dev/null
+++ b/test/unit/RowGateway/Feature/TestAsset/TestRowGatewayFeature.php
@@ -0,0 +1,29 @@
+ */
+ public array $receivedArgs = [];
+
+ public ?string $returnValue = null;
+
+ public function preInitialize(string ...$args): mixed
+ {
+ $this->called = true;
+ $this->receivedArgs = $args;
+ return $this->returnValue;
+ }
+
+ public function postInitialize(): void
+ {
+ $this->called = true;
+ }
+}
diff --git a/test/unit/Sql/AbstractSqlTest.php b/test/unit/Sql/AbstractSqlTest.php
index 005df76f..ba288fe2 100644
--- a/test/unit/Sql/AbstractSqlTest.php
+++ b/test/unit/Sql/AbstractSqlTest.php
@@ -5,16 +5,25 @@
namespace PhpDbTest\Sql;
use Override;
+use PhpDb\Adapter\Adapter;
use PhpDb\Adapter\Driver\DriverInterface;
+use PhpDb\Adapter\Driver\StatementInterface;
use PhpDb\Adapter\ParameterContainer;
use PhpDb\Adapter\StatementContainer;
use PhpDb\Sql\AbstractSql;
+use PhpDb\Sql\Argument;
use PhpDb\Sql\Argument\Identifier;
+use PhpDb\Sql\ArgumentInterface;
+use PhpDb\Sql\ArgumentType;
+use PhpDb\Sql\Exception\InvalidArgumentException;
+use PhpDb\Sql\Exception\RuntimeException;
use PhpDb\Sql\Expression;
use PhpDb\Sql\ExpressionInterface;
+use PhpDb\Sql\Join;
use PhpDb\Sql\Predicate;
use PhpDb\Sql\Select;
use PhpDb\Sql\TableIdentifier;
+use PhpDbTest\TestAsset\SelectDecorator;
use PhpDbTest\TestAsset\TrustingSql92Platform;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\Group;
@@ -39,12 +48,13 @@
#[CoversMethod(AbstractSql::class, 'buildSqlString')]
#[CoversMethod(AbstractSql::class, 'renderTable')]
#[CoversMethod(AbstractSql::class, 'processExpression')]
-#[CoversMethod(AbstractSql::class, 'processExpressionValue')]
#[CoversMethod(AbstractSql::class, 'processExpressionOrSelect')]
#[CoversMethod(AbstractSql::class, 'processExpressionParameterName')]
#[CoversMethod(AbstractSql::class, 'createSqlFromSpecificationAndParameters')]
#[CoversMethod(AbstractSql::class, 'processSubSelect')]
#[CoversMethod(AbstractSql::class, 'processJoin')]
+#[CoversMethod(AbstractSql::class, 'processIdentifiersArgument')]
+#[CoversMethod(AbstractSql::class, 'flattenExpressionValues')]
#[CoversMethod(AbstractSql::class, 'resolveColumnValue')]
#[CoversMethod(AbstractSql::class, 'resolveTable')]
#[CoversMethod(AbstractSql::class, 'localizeVariables')]
@@ -351,6 +361,63 @@ public function testProcessSubSelectWithoutParameterContainer(): void
self::assertStringContainsString('SELECT', $result);
}
+ /**
+ * @throws ReflectionException
+ */
+ public function testProcessExpressionWithValuesArgument(): void
+ {
+ $expression = new Expression(
+ '? IN (?, ?, ?)',
+ [
+ new Argument\Identifier('id'),
+ new Argument\Value(1),
+ new Argument\Value(2),
+ new Argument\Value(3),
+ ]
+ );
+
+ $sqlAndParams = $this->invokeProcessExpressionMethod($expression);
+
+ self::assertStringContainsString("'1'", $sqlAndParams);
+ self::assertStringContainsString("'2'", $sqlAndParams);
+ self::assertStringContainsString("'3'", $sqlAndParams);
+ self::assertStringContainsString('"id"', $sqlAndParams);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testProcessExpressionWithIdentifiersArgument(): void
+ {
+ $expression = new Expression('? IN (SELECT col1, col2 FROM bar)', [
+ Argument::identifiers(['col1', 'col2']),
+ ]);
+
+ $sqlAndParams = $this->invokeProcessExpressionMethod($expression);
+
+ self::assertStringContainsString('"col1"', $sqlAndParams);
+ self::assertStringContainsString('"col2"', $sqlAndParams);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testCreateSqlFromSpecificationThrowsOnParameterCountMismatch(): void
+ {
+ $method = new ReflectionMethod($this->abstractSql, 'createSqlFromSpecificationAndParameters');
+
+ $specifications = [
+ 'SELECT %1$s FROM %2$s' => [
+ [1 => '%1$s', 'combinedby' => ', '],
+ null,
+ ],
+ ];
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('A number of parameters was found that is not supported by this specification');
+ $method->invoke($this->abstractSql, $specifications, ['col1', 'table', 'extra']);
+ }
+
/**
* @throws ReflectionException
*/
@@ -369,4 +436,329 @@ protected function invokeProcessExpressionMethod(
$namedParameterPrefix
);
}
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testProcessJoinWithArrayAlias(): void
+ {
+ $join = new Join();
+ $join->join(['b' => 'bar'], 'foo.id = b.foo_id');
+
+ $method = new ReflectionMethod($this->abstractSql, 'processJoin');
+ $result = $method->invoke(
+ $this->abstractSql,
+ $join,
+ new TrustingSql92Platform(),
+ null,
+ null
+ );
+
+ self::assertNotNull($result);
+ self::assertStringContainsString('AS', $result[0][0][1]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testProcessJoinWithTableIdentifier(): void
+ {
+ $join = new Join();
+ $join->join(new TableIdentifier('bar', 'myschema'), 'foo.id = bar.foo_id');
+
+ $method = new ReflectionMethod($this->abstractSql, 'processJoin');
+ $result = $method->invoke(
+ $this->abstractSql,
+ $join,
+ new TrustingSql92Platform(),
+ null,
+ null
+ );
+
+ self::assertNotNull($result);
+ self::assertStringContainsString('"myschema"', $result[0][0][1]);
+ self::assertStringContainsString('"bar"', $result[0][0][1]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testProcessJoinWithPredicateExpressionOnClause(): void
+ {
+ $join = new Join();
+ $join->join('bar', new Predicate\Expression('foo.id = bar.foo_id AND bar.active = 1'));
+
+ $method = new ReflectionMethod($this->abstractSql, 'processJoin');
+ $result = $method->invoke(
+ $this->abstractSql,
+ $join,
+ new TrustingSql92Platform(),
+ null,
+ null
+ );
+
+ self::assertNotNull($result);
+ self::assertStringContainsString('foo.id = bar.foo_id', $result[0][0][2]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testProcessJoinReturnsNullWhenEmpty(): void
+ {
+ $method = new ReflectionMethod($this->abstractSql, 'processJoin');
+ $result = $method->invoke(
+ $this->abstractSql,
+ null,
+ new TrustingSql92Platform(),
+ null,
+ null
+ );
+
+ self::assertNull($result);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testRenderTableWithAlias(): void
+ {
+ $method = new ReflectionMethod($this->abstractSql, 'renderTable');
+ $result = $method->invoke($this->abstractSql, '"foo"', '"f"');
+
+ self::assertSame('"foo" AS "f"', $result);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testProcessJoinWithExpressionNameViaArray(): void
+ {
+ $join = new Join();
+ $join->join(['x' => new Expression('LATERAL(SELECT 1)')], 'true');
+
+ $method = new ReflectionMethod($this->abstractSql, 'processJoin');
+ $result = $method->invoke(
+ $this->abstractSql,
+ $join,
+ new TrustingSql92Platform(),
+ null,
+ null
+ );
+
+ self::assertStringContainsString('LATERAL(SELECT 1)', $result[0][0][1]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testProcessJoinWithSelectSubqueryViaArray(): void
+ {
+ $subselect = new Select('bar');
+ $join = new Join();
+ $join->join(['b' => $subselect], 'foo.id = b.foo_id');
+
+ $method = new ReflectionMethod($this->abstractSql, 'processJoin');
+ $result = $method->invoke(
+ $this->abstractSql,
+ $join,
+ new TrustingSql92Platform(),
+ null,
+ null
+ );
+
+ self::assertStringContainsString('SELECT', $result[0][0][1]);
+ self::assertStringContainsString('AS', $result[0][0][1]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testCreateSqlFromSpecWithCombinedByScalarParam(): void
+ {
+ $method = new ReflectionMethod($this->abstractSql, 'createSqlFromSpecificationAndParameters');
+
+ $spec = [
+ 'SELECT %1$s FROM %2$s' => [
+ [1 => '%1$s', 'combinedby' => ', '],
+ null,
+ ],
+ ];
+ $params = [['col1'], 'table1'];
+
+ $result = $method->invoke($this->abstractSql, $spec, $params);
+
+ self::assertSame('SELECT col1 FROM table1', $result);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testCreateSqlFromSpecWithCombinedByThrowsOnUnsupportedCount(): void
+ {
+ $method = new ReflectionMethod($this->abstractSql, 'createSqlFromSpecificationAndParameters');
+
+ $spec = [
+ 'SELECT %1$s FROM %2$s' => [
+ [1 => '%1$s', 'combinedby' => ', '],
+ null,
+ ],
+ ];
+ $params = [[['a', 'b']], 'table1'];
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('A number of parameters (2)');
+ $method->invoke($this->abstractSql, $spec, $params);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testCreateSqlFromSpecWithNonCombinedByParam(): void
+ {
+ $method = new ReflectionMethod($this->abstractSql, 'createSqlFromSpecificationAndParameters');
+
+ $spec = [
+ 'FROM %1$s' => [
+ [1 => '%1$s'],
+ ],
+ ];
+ $params = [['my_table']];
+
+ $result = $method->invoke($this->abstractSql, $spec, $params);
+
+ self::assertSame('FROM my_table', $result);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testCreateSqlFromSpecNonCombinedByThrowsOnUnsupportedCount(): void
+ {
+ $method = new ReflectionMethod($this->abstractSql, 'createSqlFromSpecificationAndParameters');
+
+ $spec = [
+ 'FROM %1$s' => [
+ [1 => '%1$s'],
+ ],
+ ];
+ $params = [['a', 'b']];
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('A number of parameters (2)');
+ $method->invoke($this->abstractSql, $spec, $params);
+ }
+
+ public function testProcessExpressionThrowsOnUnknownArgumentType(): void
+ {
+ $unknownArg = new class implements ArgumentInterface {
+ public function getType(): ArgumentType
+ {
+ return ArgumentType::Value;
+ }
+
+ public function getValue(): string
+ {
+ return 'test';
+ }
+
+ public function getSpecification(): string
+ {
+ return '%s';
+ }
+ };
+
+ $expression = new Expression('?', [$unknownArg]);
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Unknown argument type');
+ $this->invokeProcessExpressionMethod($expression);
+ }
+
+ public function testResolveColumnValueWithNamedParameterPrefix(): void
+ {
+ $select = new Select('users');
+ $select->columns(['id']);
+ $select->where(new Predicate\In('status', [1, 2]));
+
+ $mockDriver = $this->getMockBuilder(DriverInterface::class)->getMock();
+ $mockDriver->method('formatParameterName')
+ ->willReturnCallback(fn(string $name): string => ':' . $name);
+
+ $parameterContainer = new ParameterContainer();
+ $mockStatement = $this->createMock(StatementInterface::class);
+ $mockStatement->method('getParameterContainer')->willReturn($parameterContainer);
+ $mockStatement->method('setSql')->willReturnSelf();
+
+ $adapter = $this->getMockBuilder(Adapter::class)
+ ->setConstructorArgs([$mockDriver, new TrustingSql92Platform()])
+ ->getMock();
+ $adapter->method('getDriver')->willReturn($mockDriver);
+ $adapter->method('getPlatform')->willReturn(new TrustingSql92Platform());
+
+ $select->prepareStatement($adapter, $mockStatement);
+
+ self::assertGreaterThanOrEqual(2, $parameterContainer->count());
+ }
+
+ public function testLocalizeVariablesCopiesSubjectProperties(): void
+ {
+ $decorator = new SelectDecorator();
+ $select = new Select('users');
+ $select->columns(['id', 'name']);
+ $decorator->setSubject($select);
+
+ $sql = $decorator->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString('"users"', $sql);
+ self::assertStringContainsString('"id"', $sql);
+ }
+
+ public function testProcessSubSelectUsesDecoratorWhenPlatformDecorator(): void
+ {
+ $decorator = new SelectDecorator();
+ $outer = new Select('foo');
+ $outer->where(['x' => new Select('bar')]);
+
+ $decorator->setSubject($outer);
+
+ $sql = $decorator->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString('SELECT "bar"', $sql);
+ self::assertStringContainsString('SELECT "foo"', $sql);
+ }
+
+ public function testFlattenExpressionValuesViaInPredicate(): void
+ {
+ $select = new Select('users');
+ $select->where(new Predicate\In('id', [1, 2, 3]));
+
+ $sql = $select->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString("\"id\" IN ('1', '2', '3')", $sql);
+ }
+
+ public function testFlattenExpressionValuesViaInPredicateWithParameterContainer(): void
+ {
+ $select = new Select('users');
+ $select->where(new Predicate\In('id', [1, 2, 3]));
+
+ $mockDriver = $this->getMockBuilder(DriverInterface::class)->getMock();
+ $mockDriver->method('formatParameterName')
+ ->willReturnCallback(fn(string $name): string => ':' . $name);
+
+ $parameterContainer = new ParameterContainer();
+ $mockStatement = $this->createMock(StatementInterface::class);
+ $mockStatement->method('getParameterContainer')->willReturn($parameterContainer);
+
+ $adapter = $this->getMockBuilder(Adapter::class)
+ ->setConstructorArgs([$mockDriver, new TrustingSql92Platform()])
+ ->getMock();
+ $adapter->method('getDriver')->willReturn($mockDriver);
+ $adapter->method('getPlatform')->willReturn(new TrustingSql92Platform());
+
+ $select->prepareStatement($adapter, $mockStatement);
+
+ self::assertSame(3, $parameterContainer->count());
+ }
}
diff --git a/test/unit/Sql/Argument/LiteralTest.php b/test/unit/Sql/Argument/LiteralTest.php
new file mode 100644
index 00000000..784918cb
--- /dev/null
+++ b/test/unit/Sql/Argument/LiteralTest.php
@@ -0,0 +1,40 @@
+getType());
+ }
+
+ public function testGetValueReturnsLiteralString(): void
+ {
+ $literal = new Literal('NOW()');
+
+ self::assertSame('NOW()', $literal->getValue());
+ }
+
+ public function testGetSpecificationReturnsPlaceholder(): void
+ {
+ $literal = new Literal('test');
+
+ self::assertSame('%s', $literal->getSpecification());
+ }
+}
diff --git a/test/unit/Sql/ArgumentTest.php b/test/unit/Sql/ArgumentTest.php
index 40c847b8..1d94c9f2 100644
--- a/test/unit/Sql/ArgumentTest.php
+++ b/test/unit/Sql/ArgumentTest.php
@@ -16,16 +16,10 @@
use PHPUnit\Framework\TestCase;
use TypeError;
-#[CoversMethod(Argument::class, '__construct')]
-#[CoversMethod(Argument::class, 'setType')]
-#[CoversMethod(Argument::class, 'getType')]
-#[CoversMethod(Argument::class, 'setValue')]
-#[CoversMethod(Argument::class, 'getValue')]
-#[CoversMethod(Argument::class, 'getValueAsString')]
-#[CoversMethod(Argument::class, 'getSpecification')]
#[CoversMethod(Argument::class, 'value')]
#[CoversMethod(Argument::class, 'identifier')]
#[CoversMethod(Argument::class, 'literal')]
+#[CoversMethod(Argument::class, 'select')]
final class ArgumentTest extends TestCase
{
public function testConstructorWithSimpleValue(): void
@@ -128,4 +122,13 @@ public function testConstructorWithFloatValue(): void
self::assertEquals(3.14, $argument->getValue());
self::assertEquals(ArgumentType::Value, $argument->getType());
}
+
+ public function testStaticSelectMethodCreatesSelectArgument(): void
+ {
+ $select = new Select();
+ $argument = Argument::select($select);
+
+ self::assertSame($select, $argument->getValue());
+ self::assertEquals(ArgumentType::Select, $argument->getType());
+ }
}
diff --git a/test/unit/Sql/CombineTest.php b/test/unit/Sql/CombineTest.php
index 476edf22..bbe77ff7 100644
--- a/test/unit/Sql/CombineTest.php
+++ b/test/unit/Sql/CombineTest.php
@@ -193,6 +193,53 @@ public function testGetRawState(): void
);
}
+ public function testCombineWithArrayOfSelectAndModifier(): void
+ {
+ $this->combine->combine([
+ [new Select('t1'), 'UNION', 'ALL'],
+ [new Select('t2'), 'INTERSECT'],
+ ]);
+
+ self::assertEquals(
+ '(SELECT "t1".* FROM "t1") INTERSECT (SELECT "t2".* FROM "t2")',
+ $this->combine->getSqlString()
+ );
+ }
+
+ public function testAlignColumnsAppendsNullExpressionsForMissingColumns(): void
+ {
+ $select1 = new Select('t1');
+ $select1->columns(['a' => 'a']);
+
+ $select2 = new Select('t2');
+ $select2->columns(['a' => 'a', 'b' => 'b']);
+
+ $this->combine->union([$select1, $select2])->alignColumns();
+
+ $columns1 = $select1->getRawState('columns');
+ self::assertArrayHasKey('b', $columns1);
+ self::assertInstanceOf(Expression::class, $columns1['b']);
+ }
+
+ public function testConstructorWithSelectDelegatesToCombine(): void
+ {
+ $select = new Select('foo');
+ $combine = new Combine($select, Combine::COMBINE_EXCEPT, 'ALL');
+
+ $rawState = $combine->getRawState();
+ self::assertCount(1, $rawState['combine']);
+ self::assertSame('except', $rawState['combine'][0]['type']);
+ self::assertSame('ALL', $rawState['combine'][0]['modifier']);
+ }
+
+ public function testAlignColumnsReturnsEarlyWhenEmpty(): void
+ {
+ $combine = new Combine();
+ $result = $combine->alignColumns();
+
+ self::assertSame($combine, $result);
+ }
+
protected function getMockAdapter(): Adapter|MockObject
{
$parameterContainer = new ParameterContainer();
diff --git a/test/unit/Sql/Ddl/Column/ColumnTest.php b/test/unit/Sql/Ddl/Column/ColumnTest.php
index d32556e6..d17c3119 100644
--- a/test/unit/Sql/Ddl/Column/ColumnTest.php
+++ b/test/unit/Sql/Ddl/Column/ColumnTest.php
@@ -8,7 +8,9 @@
use PhpDb\Sql\Argument\Literal;
use PhpDb\Sql\Argument\Value;
use PhpDb\Sql\Ddl\Column\Column;
+use PhpDb\Sql\Ddl\Constraint\PrimaryKey;
use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[CoversMethod(Column::class, '__construct')]
@@ -23,6 +25,7 @@
#[CoversMethod(Column::class, 'getOptions')]
#[CoversMethod(Column::class, 'addConstraint')]
#[CoversMethod(Column::class, 'getExpressionData')]
+#[Group('unit')]
final class ColumnTest extends TestCase
{
public function testConstructor(): void
@@ -278,4 +281,33 @@ public function testGetExpressionDataWithBoolDefault(): void
Argument::value(false),
], $expressionData['values']);
}
+
+ public function testAddConstraintAppendsConstraintToColumn(): void
+ {
+ $column = new Column('id');
+
+ $result = $column->addConstraint(new PrimaryKey());
+
+ self::assertSame($column, $result);
+ }
+
+ public function testGetExpressionDataIncludesConstraints(): void
+ {
+ $column = new Column('id');
+ $column->addConstraint(new PrimaryKey());
+
+ $expressionData = $column->getExpressionData();
+
+ self::assertStringContainsString('PRIMARY KEY', $expressionData['spec']);
+ }
+
+ public function testGetExpressionDataIncludesConstraintValues(): void
+ {
+ $column = new Column('id');
+ $column->addConstraint(new PrimaryKey('id', 'pk_id'));
+
+ $expressionData = $column->getExpressionData();
+
+ self::assertNotEmpty($expressionData['values']);
+ }
}
diff --git a/test/unit/Sql/Ddl/Column/IntegerTest.php b/test/unit/Sql/Ddl/Column/IntegerTest.php
index d86c6653..b8f69a77 100644
--- a/test/unit/Sql/Ddl/Column/IntegerTest.php
+++ b/test/unit/Sql/Ddl/Column/IntegerTest.php
@@ -9,10 +9,13 @@
use PhpDb\Sql\Ddl\Column\Integer;
use PhpDb\Sql\Ddl\Constraint\PrimaryKey;
use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[CoversMethod(Integer::class, '__construct')]
+#[CoversMethod(Integer::class, 'getExpressionData')]
#[CoversMethod(Column::class, 'getExpressionData')]
+#[Group('unit')]
final class IntegerTest extends TestCase
{
public function testObjectConstruction(): void
@@ -44,4 +47,23 @@ public function testGetExpressionData(): void
Argument::literal('INTEGER'),
], $expressionData['values']);
}
+
+ public function testGetExpressionDataIncludesLengthWhenOptionSet(): void
+ {
+ $column = new Integer('id');
+ $column->setOption('length', '11');
+
+ $expressionData = $column->getExpressionData();
+
+ self::assertStringContainsString('(11)', $expressionData['spec']);
+ }
+
+ public function testGetExpressionDataExcludesLengthWhenNotSet(): void
+ {
+ $column = new Integer('id');
+
+ $expressionData = $column->getExpressionData();
+
+ self::assertStringNotContainsString('(', $expressionData['spec']);
+ }
}
diff --git a/test/unit/Sql/ExpressionTest.php b/test/unit/Sql/ExpressionTest.php
index d01f7016..2ee05788 100644
--- a/test/unit/Sql/ExpressionTest.php
+++ b/test/unit/Sql/ExpressionTest.php
@@ -197,7 +197,6 @@ public function testGetExpressionDataThrowsExceptionWhenParameterCountMismatch()
public function testConstructorWithMultipleArguments(): void
{
- // Test deprecated multi-argument constructor
$expression = new Expression('? + ? - ?', 1, 2, 3);
$expressionData = $expression->getExpressionData();
@@ -209,4 +208,35 @@ public function testConstructorWithMultipleArguments(): void
Argument::value(3),
], $expressionData['values']);
}
+
+ public function testSetExpressionThrowsOnEmptyString(): void
+ {
+ $expression = new Expression();
+
+ $this->expectException(InvalidArgumentException::class);
+ $expression->setExpression('');
+ }
+
+ public function testSetParametersWrapsArrayInValuesArgument(): void
+ {
+ $expression = new Expression('? IN (?)', [Argument::identifier('id'), [1, 2, 3]]);
+
+ $data = $expression->getExpressionData();
+
+ self::assertCount(2, $data['values']);
+ self::assertInstanceOf(Argument\Values::class, $data['values'][1]);
+ }
+
+ public function testGetExpressionDataUsesRegexWhenPlaceholderCountMismatches(): void
+ {
+ $expression = new Expression('uf.user_id = :user_id OR uf.friend_id = :user_id', ['user_id' => 1]);
+
+ $expressionData = $expression->getExpressionData();
+
+ self::assertEquals(
+ 'uf.user_id = :user_id OR uf.friend_id = :user_id',
+ $expressionData['spec']
+ );
+ self::assertCount(1, $expressionData['values']);
+ }
}
diff --git a/test/unit/Sql/InsertTest.php b/test/unit/Sql/InsertTest.php
index b1865ae8..cbc4d745 100644
--- a/test/unit/Sql/InsertTest.php
+++ b/test/unit/Sql/InsertTest.php
@@ -6,6 +6,7 @@
use Override;
use PhpDb\Adapter\Driver\DriverInterface;
+use PhpDb\Adapter\Driver\PdoDriverInterface;
use PhpDb\Adapter\Driver\StatementInterface;
use PhpDb\Adapter\ParameterContainer;
use PhpDb\Adapter\StatementContainer;
@@ -440,4 +441,25 @@ public function testPrepareStatementCreatesParameterContainerWhenNotPresent(): v
self::assertSame($mockStatement, $result);
}
+
+ public function testProcessInsertWithPdoDriverUsesDriverColumnQuoting(): void
+ {
+ $mockDriver = $this->getMockBuilder(PdoDriverInterface::class)->getMock();
+ $mockDriver->expects($this->any())->method('getPrepareType')->willReturn('positional');
+ $mockDriver->expects($this->any())
+ ->method('formatParameterName')
+ ->willReturnCallback(fn(string $name): string => ':' . $name);
+ $mockAdapter = $this->createMockAdapter($mockDriver);
+
+ $mockStatement = new StatementContainer();
+
+ $this->insert->into('foo')
+ ->values(['bar' => 'baz', 'boo' => 'qux']);
+
+ $this->insert->prepareStatement($mockAdapter, $mockStatement);
+
+ $sql = $mockStatement->getSql();
+ self::assertStringContainsString(':c_0', $sql);
+ self::assertStringContainsString(':c_1', $sql);
+ }
}
diff --git a/test/unit/Sql/JoinTest.php b/test/unit/Sql/JoinTest.php
index 9bd2b331..4faaa59b 100644
--- a/test/unit/Sql/JoinTest.php
+++ b/test/unit/Sql/JoinTest.php
@@ -4,6 +4,7 @@
namespace PhpDbTest\Sql;
+use PhpDb\Sql\Exception\InvalidArgumentException;
use PhpDb\Sql\Join;
use PhpDb\Sql\Select;
use PhpDbTest\DeprecatedAssertionsTrait;
@@ -17,7 +18,6 @@
#[IgnoreDeprecations]
#[RequiresPhp('<= 8.6')]
-#[CoversMethod(Join::class, '__construct')]
#[CoversMethod(Join::class, 'rewind')]
#[CoversMethod(Join::class, 'current')]
#[CoversMethod(Join::class, 'key')]
@@ -154,4 +154,13 @@ public function testReset(): void
self::assertEquals(0, $join->count());
}
+
+ public function testJoinThrowsOnInvalidMultiElementArray(): void
+ {
+ $join = new Join();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage("expects 'b' as a single element associative array");
+ $join->join(['a' => 'b', 'c' => 'd'], 'on');
+ }
}
diff --git a/test/unit/Sql/Platform/AbstractPlatformTest.php b/test/unit/Sql/Platform/AbstractPlatformTest.php
new file mode 100644
index 00000000..fe5429e8
--- /dev/null
+++ b/test/unit/Sql/Platform/AbstractPlatformTest.php
@@ -0,0 +1,137 @@
+platform = new AbstractPlatform();
+ }
+
+ public function testSetSubjectReturnsStatic(): void
+ {
+ $subject = $this->createMock(SqlInterface::class);
+ $result = $this->platform->setSubject($subject);
+
+ self::assertSame($this->platform, $result);
+ }
+
+ public function testSetAndGetTypeDecorator(): void
+ {
+ $decorator = $this->createMock(PlatformDecoratorInterface::class);
+ $this->platform->setTypeDecorator(Select::class, $decorator);
+
+ $decorators = $this->platform->getDecorators();
+
+ self::assertArrayHasKey(Select::class, $decorators);
+ self::assertSame($decorator, $decorators[Select::class]);
+ }
+
+ public function testGetTypeDecoratorReturnsSubjectWhenNoMatch(): void
+ {
+ $subject = $this->createMock(SqlInterface::class);
+
+ $result = $this->platform->getTypeDecorator($subject);
+
+ self::assertSame($subject, $result);
+ }
+
+ public function testGetTypeDecoratorLoopMatchesByInstanceof(): void
+ {
+ $decorator = $this->createMock(PlatformDecoratorInterface::class);
+ $decorator->method('setSubject')->willReturnSelf();
+
+ $this->platform->setTypeDecorator(Select::class, $decorator);
+
+ $subject = new Select('foo');
+ $result = $this->platform->getTypeDecorator($subject);
+
+ self::assertSame($decorator, $result);
+ }
+
+ public function testPrepareStatementThrowsWhenSubjectNotPreparable(): void
+ {
+ $subject = $this->createMock(SqlInterface::class);
+ $this->platform->setSubject($subject);
+
+ $adapter = $this->createMock(AdapterInterface::class);
+ $statement = new StatementContainer();
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('The subject does not appear to implement');
+ $this->platform->prepareStatement($adapter, $statement);
+ }
+
+ public function testGetSqlStringThrowsWhenSubjectNotSqlInterface(): void
+ {
+ $subject = $this->createMock(PreparableSqlInterface::class);
+ $this->platform->setSubject($subject);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('The subject does not appear to implement');
+ $this->platform->getSqlString();
+ }
+
+ public function testGetSqlStringDelegatesToDecoratorSubject(): void
+ {
+ $select = new Select('foo');
+ $this->platform->setSubject($select);
+
+ $sql = $this->platform->getSqlString();
+
+ self::assertStringContainsString('SELECT', $sql);
+ self::assertStringContainsString('"foo"', $sql);
+ }
+
+ public function testPrepareStatementDelegatesToDecoratorSubject(): void
+ {
+ $select = new Select('foo');
+ $select->where(['id' => 1]);
+ $this->platform->setSubject($select);
+
+ $mockPlatform = $this->createMock(PlatformInterface::class);
+ $mockPlatform->method('quoteIdentifier')->willReturnCallback(fn($v) => '"' . $v . '"');
+ $mockPlatform->method('quoteIdentifierInFragment')->willReturnCallback(fn($v) => '"' . $v . '"');
+ $mockPlatform->method('getIdentifierSeparator')->willReturn('.');
+ $mockPlatform->method('getSqlPlatformDecorator')->willReturn($this->platform);
+
+ $mockDriver = $this->createMock(DriverInterface::class);
+ $mockDriver->method('formatParameterName')->willReturn('?');
+
+ $adapter = $this->getMockBuilder(AdapterInterface::class)->getMock();
+ $adapter->method('getPlatform')->willReturn($mockPlatform);
+ $adapter->method('getDriver')->willReturn($mockDriver);
+
+ $statement = new StatementContainer();
+ $result = $this->platform->prepareStatement($adapter, $statement);
+
+ self::assertSame($statement, $result);
+ self::assertStringContainsString('SELECT', $statement->getSql());
+ }
+}
diff --git a/test/unit/Sql/Platform/PlatformTest.php b/test/unit/Sql/Platform/PlatformTest.php
index eb3be938..ae6066dc 100644
--- a/test/unit/Sql/Platform/PlatformTest.php
+++ b/test/unit/Sql/Platform/PlatformTest.php
@@ -5,12 +5,21 @@
namespace PhpDbTest\Sql\Platform;
use PhpDb\Adapter\Adapter;
+use PhpDb\Adapter\AdapterInterface;
use PhpDb\Adapter\Driver\DriverInterface;
+use PhpDb\Adapter\Platform\PlatformInterface;
use PhpDb\Adapter\StatementContainer;
use PhpDb\ResultSet\ResultSet;
+use PhpDb\Sql\Exception\RuntimeException;
+use PhpDb\Sql\Insert;
+use PhpDb\Sql\Platform\AbstractPlatform;
use PhpDb\Sql\Platform\Platform;
+use PhpDb\Sql\Platform\PlatformDecoratorInterface;
+use PhpDb\Sql\PreparableSqlInterface;
+use PhpDb\Sql\Select;
+use PhpDb\Sql\SqlInterface;
use PhpDbTest\TestAsset;
-use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\MockObject\MockObject;
@@ -20,6 +29,15 @@
#[IgnoreDeprecations]
#[RequiresPhp('<= 8.6')]
+#[CoversMethod(Platform::class, '__construct')]
+#[CoversMethod(Platform::class, 'setTypeDecorator')]
+#[CoversMethod(Platform::class, 'getTypeDecorator')]
+#[CoversMethod(Platform::class, 'getDecorators')]
+#[CoversMethod(Platform::class, 'prepareStatement')]
+#[CoversMethod(Platform::class, 'getSqlString')]
+#[CoversMethod(Platform::class, 'resolvePlatformName')]
+#[CoversMethod(Platform::class, 'resolvePlatform')]
+#[CoversMethod(Platform::class, 'getDefaultPlatform')]
class PlatformTest extends TestCase
{
/**
@@ -53,22 +71,6 @@ public function testResolvePlatformName(): void
self::assertEquals('sql92', $reflectionMethod->invoke($platform, new TestAsset\TrustingSql92Platform()));
}
- #[Group('6890')]
- public function testAbstractPlatformCrashesGracefullyOnMissingDefaultPlatform(): void
- {
- $this->markTestSkipped(
- 'Cannot modify readonly properties in Adapter - test is incompatible with readonly properties'
- );
- }
-
- #[Group('6890')]
- public function testAbstractPlatformCrashesGracefullyOnMissingDefaultPlatformWithGetDecorators(): void
- {
- $this->markTestSkipped(
- 'Cannot modify readonly properties in Adapter - test is incompatible with readonly properties'
- );
- }
-
protected function resolveAdapter(string $platformName): Adapter
{
$platform = null;
@@ -100,4 +102,170 @@ protected function resolveAdapter(string $platformName): Adapter
return new Adapter($mockDriver, $platform, new ResultSet());
}
+
+ public function testSetTypeDecoratorRegistersDecorator(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $decorator = $this->createMock(PlatformDecoratorInterface::class);
+ $platform->setTypeDecorator(Select::class, $decorator);
+
+ $decorators = $platform->getDecorators();
+ self::assertArrayHasKey(Select::class, $decorators);
+ self::assertSame($decorator, $decorators[Select::class]);
+ }
+
+ public function testGetTypeDecoratorReturnsSubjectWhenNoDecoratorRegistered(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $select = new Select('foo');
+ $result = $platform->getTypeDecorator($select);
+
+ self::assertSame($select, $result);
+ }
+
+ public function testGetDefaultPlatformReturnsInstance(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $reflectionMethod = new ReflectionMethod($platform, 'getDefaultPlatform');
+ $result = $reflectionMethod->invoke($platform);
+
+ self::assertSame($adapterPlatform, $result);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testResolvePlatformNameCachesResult(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $reflectionMethod = new ReflectionMethod($platform, 'resolvePlatformName');
+
+ $first = $reflectionMethod->invoke($platform, null);
+ $second = $reflectionMethod->invoke($platform, null);
+
+ self::assertEquals($first, $second);
+ self::assertEquals('sql92', $first);
+ }
+
+ public function testResolvePlatformWithAdapterInterface(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $mockPlatform = $this->createMock(PlatformInterface::class);
+ $mockPlatform->method('getName')->willReturn('TestPlatform');
+
+ $mockAdapter = $this->createMock(AdapterInterface::class);
+ $mockAdapter->expects($this->once())->method('getPlatform')->willReturn($mockPlatform);
+
+ $reflectionMethod = new ReflectionMethod($platform, 'resolvePlatform');
+ $result = $reflectionMethod->invoke($platform, $mockAdapter);
+
+ self::assertSame($mockPlatform, $result);
+ }
+
+ public function testResolvePlatformWithPlatformInterface(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $mockPlatform = $this->createMock(PlatformInterface::class);
+
+ $reflectionMethod = new ReflectionMethod($platform, 'resolvePlatform');
+ $result = $reflectionMethod->invoke($platform, $mockPlatform);
+
+ self::assertSame($mockPlatform, $result);
+ }
+
+ public function testPrepareStatementThrowsWhenSubjectNotPreparable(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $subject = $this->createMock(SqlInterface::class);
+ $platform->setSubject($subject);
+
+ $adapter = $this->resolveAdapter('sql92');
+ $statement = new StatementContainer();
+
+ $this->expectException(RuntimeException::class);
+ $platform->prepareStatement($adapter, $statement);
+ }
+
+ public function testGetSqlStringThrowsWhenSubjectNotSqlInterface(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $subject = $this->createMock(PreparableSqlInterface::class);
+ $platform->setSubject($subject);
+
+ $this->expectException(RuntimeException::class);
+ $platform->getSqlString($adapterPlatform);
+ }
+
+ public function testGetSqlStringDelegatesToTypeDecorator(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $select = new Select('foo');
+ $platform->setSubject($select);
+
+ $sql = $platform->getSqlString($adapterPlatform);
+
+ self::assertStringContainsString('SELECT', $sql);
+ self::assertStringContainsString('"foo"', $sql);
+ }
+
+ public function testGetTypeDecoratorMatchesExactClass(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $decorator = $this->createMock(PlatformDecoratorInterface::class);
+ $decorator->expects(self::once())->method('setSubject');
+ $platform->setTypeDecorator(Select::class, $decorator);
+
+ $select = new Select('foo');
+ $result = $platform->getTypeDecorator($select);
+
+ self::assertSame($decorator, $result);
+ }
+
+ public function testGetTypeDecoratorFallsThroughWhenNoMatch(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $decorator = $this->createMock(PlatformDecoratorInterface::class);
+ $platform->setTypeDecorator(Insert::class, $decorator);
+
+ $select = new Select('foo');
+ $result = $platform->getTypeDecorator($select);
+
+ self::assertSame($select, $result);
+ }
+
+ public function testGetTypeDecoratorMatchesByInstanceofLoop(): void
+ {
+ $adapterPlatform = new TestAsset\TrustingSql92Platform();
+ $platform = new Platform($adapterPlatform);
+
+ $innerPlatform = new AbstractPlatform();
+ $platform->setTypeDecorator(SqlInterface::class, $innerPlatform);
+
+ $select = new Select('foo');
+ $result = $platform->getTypeDecorator($select);
+
+ self::assertSame($innerPlatform, $result);
+ }
}
diff --git a/test/unit/Sql/Predicate/InTest.php b/test/unit/Sql/Predicate/InTest.php
index ec4b8212..691eacdc 100644
--- a/test/unit/Sql/Predicate/InTest.php
+++ b/test/unit/Sql/Predicate/InTest.php
@@ -5,12 +5,15 @@
namespace PhpDbTest\Sql\Predicate;
use PhpDb\Sql\Argument;
+use PhpDb\Sql\Argument\Select as ArgumentSelect;
+use PhpDb\Sql\Argument\Values;
use PhpDb\Sql\ArgumentInterface;
use PhpDb\Sql\ArgumentType;
use PhpDb\Sql\Exception\InvalidArgumentException;
use PhpDb\Sql\Predicate\In;
use PhpDb\Sql\Select;
use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[CoversMethod(In::class, '__construct')]
@@ -19,6 +22,7 @@
#[CoversMethod(In::class, 'setValueSet')]
#[CoversMethod(In::class, 'getValueSet')]
#[CoversMethod(In::class, 'getExpressionData')]
+#[Group('unit')]
final class InTest extends TestCase
{
public function testEmptyConstructorYieldsNullIdentifierAndValueSet(): void
@@ -275,4 +279,26 @@ public function testGetExpressionDataThrowsExceptionWhenValueSetNotSet(): void
$this->expectExceptionMessage('Value set must be provided for IN predicate');
$in->getExpressionData();
}
+
+ public function testSetValueSetWithSelectWrapsInArgumentSelect(): void
+ {
+ $in = new In();
+ $select = new Select('users');
+
+ $in->setValueSet($select);
+
+ $valueSet = $in->getValueSet();
+ self::assertInstanceOf(ArgumentSelect::class, $valueSet);
+ self::assertSame(ArgumentType::Select, $valueSet->getType());
+ }
+
+ public function testSetValueSetWithArgumentInterfacePassesThrough(): void
+ {
+ $in = new In();
+ $values = new Values([1, 2, 3]);
+
+ $in->setValueSet($values);
+
+ self::assertSame($values, $in->getValueSet());
+ }
}
diff --git a/test/unit/Sql/Predicate/IsNullTest.php b/test/unit/Sql/Predicate/IsNullTest.php
index 46343d35..4659bf48 100644
--- a/test/unit/Sql/Predicate/IsNullTest.php
+++ b/test/unit/Sql/Predicate/IsNullTest.php
@@ -4,12 +4,14 @@
namespace PhpDbTest\Sql\Predicate;
+use PhpDb\Sql\Argument\Identifier;
use PhpDb\Sql\ArgumentInterface;
use PhpDb\Sql\ArgumentType;
use PhpDb\Sql\Exception\InvalidArgumentException;
use PhpDb\Sql\Predicate\IsNotNull;
use PhpDb\Sql\Predicate\IsNull;
use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[CoversMethod(IsNull::class, '__construct')]
@@ -18,6 +20,7 @@
#[CoversMethod(IsNull::class, 'setSpecification')]
#[CoversMethod(IsNull::class, 'getSpecification')]
#[CoversMethod(IsNull::class, 'getExpressionData')]
+#[Group('unit')]
final class IsNullTest extends TestCase
{
public function testEmptyConstructorYieldsNullIdentifier(): void
@@ -104,4 +107,25 @@ public function testGetExpressionDataThrowsExceptionWhenIdentifierNotSet(): void
$this->expectExceptionMessage('Identifier must be specified');
$isNull->getExpressionData();
}
+
+ public function testSetIdentifierWithStringConvertsToIdentifier(): void
+ {
+ $isNull = new IsNull();
+
+ $isNull->setIdentifier('foo');
+
+ $identifier = $isNull->getIdentifier();
+ self::assertInstanceOf(Identifier::class, $identifier);
+ self::assertSame('foo', $identifier->getValue());
+ }
+
+ public function testSetIdentifierWithArgumentInterfacePassesThrough(): void
+ {
+ $isNull = new IsNull();
+ $identifier = new Identifier('bar');
+
+ $isNull->setIdentifier($identifier);
+
+ self::assertSame($identifier, $isNull->getIdentifier());
+ }
}
diff --git a/test/unit/Sql/Predicate/OperatorTest.php b/test/unit/Sql/Predicate/OperatorTest.php
index 7f4b647e..6afeaf4d 100644
--- a/test/unit/Sql/Predicate/OperatorTest.php
+++ b/test/unit/Sql/Predicate/OperatorTest.php
@@ -5,12 +5,15 @@
namespace PhpDbTest\Sql\Predicate;
use PhpDb\Sql\Argument\Identifier;
+use PhpDb\Sql\Argument\Select;
use PhpDb\Sql\Argument\Value;
use PhpDb\Sql\ArgumentInterface;
use PhpDb\Sql\ArgumentType;
use PhpDb\Sql\Exception\InvalidArgumentException;
+use PhpDb\Sql\Expression;
use PhpDb\Sql\Predicate\Operator;
use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[CoversMethod(Operator::class, '__construct')]
@@ -21,6 +24,7 @@
#[CoversMethod(Operator::class, 'getRight')]
#[CoversMethod(Operator::class, 'setRight')]
#[CoversMethod(Operator::class, 'getExpressionData')]
+#[Group('unit')]
final class OperatorTest extends TestCase
{
public function testEmptyConstructorYieldsNullLeftAndRightValues(): void
@@ -187,4 +191,28 @@ public function testGetExpressionDataThrowsExceptionWhenRightNotSet(): void
$this->expectExceptionMessage('Right expression must be specified');
$operator->getExpressionData();
}
+
+ public function testSetLeftWithExpressionInterfaceWrapsInSelect(): void
+ {
+ $operator = new Operator();
+ $expression = new Expression('NOW()');
+
+ $operator->setLeft($expression);
+
+ $left = $operator->getLeft();
+ self::assertInstanceOf(Select::class, $left);
+ self::assertSame(ArgumentType::Select, $left->getType());
+ }
+
+ public function testSetRightWithExpressionInterfaceWrapsInSelect(): void
+ {
+ $operator = new Operator();
+ $expression = new Expression('NOW()');
+
+ $operator->setRight($expression);
+
+ $right = $operator->getRight();
+ self::assertInstanceOf(Select::class, $right);
+ self::assertSame(ArgumentType::Select, $right->getType());
+ }
}
diff --git a/test/unit/Sql/Predicate/PredicateSetTest.php b/test/unit/Sql/Predicate/PredicateSetTest.php
index 1b5f5a26..d5e36985 100644
--- a/test/unit/Sql/Predicate/PredicateSetTest.php
+++ b/test/unit/Sql/Predicate/PredicateSetTest.php
@@ -5,6 +5,7 @@
namespace PhpDbTest\Sql\Predicate;
use PhpDb\Sql\Argument;
+use PhpDb\Sql\Exception\InvalidArgumentException;
use PhpDb\Sql\Expression as SqlExpression;
use PhpDb\Sql\Predicate\Expression;
use PhpDb\Sql\Predicate\In;
@@ -12,6 +13,7 @@
use PhpDb\Sql\Predicate\IsNull;
use PhpDb\Sql\Predicate\Literal;
use PhpDb\Sql\Predicate\Operator;
+use PhpDb\Sql\Predicate\PredicateInterface;
use PhpDb\Sql\Predicate\PredicateSet;
use PhpDbTest\DeprecatedAssertionsTrait;
use PHPUnit\Framework\Attributes\CoversMethod;
@@ -211,4 +213,33 @@ public function testAddPredicatesWithMultipleExpressions(): void
self::assertInstanceOf(Expression::class, $predicates[0][1]);
self::assertInstanceOf(Expression::class, $predicates[1][1]);
}
+
+ public function testAddPredicateThrowsOnInvalidCombination(): void
+ {
+ $predicateSet = new PredicateSet();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage("Invalid combination: expected 'AND' or 'OR'");
+ $predicateSet->addPredicate(new IsNull('foo'), 'XOR');
+ }
+
+ public function testAddPredicatesThrowsWhenStringKeyUsedWithPredicateInterface(): void
+ {
+ $predicateSet = new PredicateSet();
+ $mock = $this->createMock(PredicateInterface::class);
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Using Predicate must not use string keys');
+ $predicateSet->addPredicates(['key' => $mock]);
+ }
+
+ public function testGetExpressionDataReturnsEmptyWhenNoPredicates(): void
+ {
+ $predicateSet = new PredicateSet();
+
+ $expressionData = $predicateSet->getExpressionData();
+
+ self::assertSame('', $expressionData['spec']);
+ self::assertSame([], $expressionData['values']);
+ }
}
diff --git a/test/unit/Sql/Predicate/PredicateTest.php b/test/unit/Sql/Predicate/PredicateTest.php
index 1912d191..91eabbc9 100644
--- a/test/unit/Sql/Predicate/PredicateTest.php
+++ b/test/unit/Sql/Predicate/PredicateTest.php
@@ -8,12 +8,16 @@
use PhpDb\Adapter\Exception\VunerablePlatformQuoteException;
use PhpDb\Adapter\Platform\Sql92;
use PhpDb\Sql\Argument;
+use PhpDb\Sql\Exception\RuntimeException;
use PhpDb\Sql\Expression;
use PhpDb\Sql\Predicate\Predicate;
+use PhpDb\Sql\Predicate\PredicateInterface;
use PhpDb\Sql\Select;
+use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
+#[Group('unit')]
final class PredicateTest extends TestCase
{
public function testEqualToCreatesOperatorPredicate(): void
@@ -373,28 +377,49 @@ public function testLiteral(): void
self::assertEquals([$expression], $expressionData['values']);
}
- /**
- * removed throws ErrorException to fix phpstan issue
- */
- public function testCanCreateExpressionsWithoutAnyBoundSqlParameters(): void
+ public function testUnnestThrowsWhenNotNested(): void
{
- $this->markTestSkipped(
- 'This test is skipped because it triggers exception in quoteValue(). That can not be expected currently.'
- );
+ $predicate = new Predicate();
- /** @phpstan-ignore deadCode.unreachable */
- $where1 = new Predicate();
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Not nested');
+ $predicate->unnest();
+ }
- $where1->expression('some_expression()');
+ public function testPredicateMethodAddsCustomPredicateInterface(): void
+ {
+ $predicate = new Predicate();
+ $mock = $this->createMock(PredicateInterface::class);
- $actual = $this->makeSqlString($where1);
+ $result = $predicate->predicate($mock);
- self::assertSame(
- 'SELECT "a_table".* FROM "a_table" WHERE (some_expression())',
- $actual
- );
+ self::assertSame($predicate, $result);
+ self::assertCount(1, $predicate);
+ }
+
+ public function testMagicGetNestReturnsNestedPredicate(): void
+ {
+ $predicate = new Predicate();
+
+ $nested = $predicate->nest;
+
+ self::assertInstanceOf(Predicate::class, $nested);
+ self::assertNotSame($predicate, $nested);
}
+ public function testMagicGetUnnestReturnsParentPredicate(): void
+ {
+ $predicate = new Predicate();
+
+ $nested = $predicate->nest;
+ $parent = $nested->unnest;
+
+ self::assertSame($predicate, $parent);
+ }
+
+ /**
+ * removed throws ErrorException to fix phpstan issue
+ */
/**
* @throws ErrorException
*/
diff --git a/test/unit/Sql/SelectTest.php b/test/unit/Sql/SelectTest.php
index 0f8ea959..1dcd508b 100644
--- a/test/unit/Sql/SelectTest.php
+++ b/test/unit/Sql/SelectTest.php
@@ -68,6 +68,9 @@
#[CoversMethod(Select::class, 'processLimit')]
#[CoversMethod(Select::class, 'processOffset')]
#[CoversMethod(Select::class, 'processCombine')]
+#[CoversMethod(Select::class, 'processStatementStart')]
+#[CoversMethod(Select::class, 'processStatementEnd')]
+#[CoversMethod(Select::class, 'resolveTable')]
final class SelectTest extends TestCase
{
use AdapterTestTrait;
@@ -208,29 +211,6 @@ public function testBadJoin(): void
$select->join(['foo'], 'x = y');
}
- /**
- * @throws ReflectionException
- */
- #[TestDox('unit test: Test processJoins() exception with bad join name')]
- public function testBadJoinName(): void
- {
- $mockExpression = $this->getMockBuilder(ExpressionInterface::class)
- ->getMock();
- $mockDriver = $this->getMockBuilder(DriverInterface::class)->getMock();
- $mockDriver->expects($this->any())->method('formatParameterName')->willReturn('?');
- $parameterContainer = new ParameterContainer();
-
- $select = new Select();
- $select->join(['foo' => $mockExpression], 'x = y');
-
- $sr = new ReflectionObject($select);
-
- $mr = $sr->getMethod('processJoins');
-
- $this->expectException(InvalidArgumentException::class);
- $mr->invokeArgs($select, [new Sql92(), $mockDriver, $parameterContainer]);
- }
-
#[TestDox('unit test: Test where() returns Select object (is chainable)')]
public function testWhereReturnsSameSelectObject(): void
{
@@ -1574,4 +1554,177 @@ public function testGetThrowsExceptionForInvalidProperty(): void
/** @noinspection ALL */
$value = $select->invalidProperty; /** @phpstan-ignore-line */
}
+
+ public function testCombineWrapsStatementInParentheses(): void
+ {
+ $select1 = new Select();
+ $select1->from('t1');
+
+ $select2 = new Select();
+ $select2->from('t2');
+
+ $select1->combine($select2);
+
+ $sql = $select1->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString('( SELECT', $sql);
+ self::assertStringContainsString('UNION', $sql);
+ self::assertStringContainsString(') UNION (', $sql);
+ }
+
+ public function testOrderWithStringContainingDirection(): void
+ {
+ $select = new Select();
+ $select->from('foo')->order('name DESC');
+
+ $sql = $select->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString('ORDER BY "name" DESC', $sql);
+ }
+
+ public function testOrderWithExpressionObject(): void
+ {
+ $select = new Select();
+ $select->from('foo')->order(new Expression('RAND()'));
+
+ $sql = $select->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString('ORDER BY RAND()', $sql);
+ }
+
+ public function testResetThrowsOnInvalidPart(): void
+ {
+ $select = new Select();
+ $result = $select->reset('invalid');
+
+ self::assertSame($select, $result);
+ }
+
+ public function testColumnsWithPrefixDisabled(): void
+ {
+ $select = new Select();
+ $select->from('foo')->columns(['col'], false);
+
+ $sql = $select->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringNotContainsString('"foo"."col"', $sql);
+ self::assertStringContainsString('"col"', $sql);
+ }
+
+ public function testGetRawStateInitializesLazyProperties(): void
+ {
+ $select = new Select();
+ $rawState = $select->getRawState();
+
+ self::assertInstanceOf(Where::class, $rawState[Select::WHERE]);
+ self::assertInstanceOf(Join::class, $rawState[Select::JOINS]);
+ self::assertInstanceOf(Having::class, $rawState[Select::HAVING]);
+ }
+
+ public function testCombineThrowsWhenAlreadyCombined(): void
+ {
+ $select = new Select();
+ $select->from('t1');
+ $select->combine(new Select('t2'));
+
+ $this->expectException(InvalidArgumentException::class);
+ $select->combine(new Select('t3'));
+ }
+
+ public function testResetTableThrowsWhenTableReadOnly(): void
+ {
+ $select = new Select('foo');
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('read only');
+ $select->reset(Select::TABLE);
+ }
+
+ public function testResetQuantifier(): void
+ {
+ $select = new Select();
+ $select->from('foo')->quantifier(Select::QUANTIFIER_DISTINCT);
+ $select->reset(Select::QUANTIFIER);
+
+ self::assertNull($select->getRawState(Select::QUANTIFIER));
+ }
+
+ public function testResetCombine(): void
+ {
+ $select = new Select();
+ $select->from('t1');
+ $select->combine(new Select('t2'));
+ $select->reset(Select::COMBINE);
+
+ self::assertEmpty($select->getRawState(Select::COMBINE));
+ }
+
+ public function testSetSpecificationStoresValidSpecification(): void
+ {
+ $select = new Select();
+ $select->setSpecification('Select', 'CUSTOM %1$s FROM %2$s');
+
+ $rawState = (new ReflectionObject($select))->getProperty('specifications');
+ self::assertSame('CUSTOM %1$s FROM %2$s', $rawState->getValue($select)['Select']);
+ }
+
+ public function testMagicGetJoinsReturnsJoinInstance(): void
+ {
+ $select = new Select();
+
+ self::assertInstanceOf(Join::class, $select->joins);
+ }
+
+ public function testCloneDeepCopiesAllSubObjects(): void
+ {
+ $select = new Select();
+ $select->from('foo');
+ $select->where('id = 1');
+ $select->having('cnt > 0');
+ $select->join('bar', 'foo.id = bar.foo_id');
+
+ $clone = clone $select;
+ $clone->where('id = 2');
+ $clone->having('cnt > 1');
+ $clone->join('baz', 'foo.id = baz.foo_id');
+
+ self::assertCount(1, $select->where);
+ self::assertCount(2, $clone->where);
+ self::assertCount(1, $select->having);
+ self::assertCount(2, $clone->having);
+ self::assertCount(1, $select->joins);
+ self::assertCount(2, $clone->joins);
+ }
+
+ public function testOrderWithAssociativeArray(): void
+ {
+ $select = new Select();
+ $select->from('foo')->order(['name' => 'DESC']);
+
+ $sql = $select->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString('ORDER BY "name" DESC', $sql);
+ }
+
+ public function testFromWithAliasArrayResolvesTableAndAlias(): void
+ {
+ $select = new Select();
+ $select->from(['a' => 'users'])->columns(['id']);
+
+ $sql = $select->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString('"users" AS "a"', $sql);
+ self::assertStringContainsString('"a"."id"', $sql);
+ }
+
+ public function testColumnsWithPrefixDisabledOmitsTablePrefix(): void
+ {
+ $select = new Select();
+ $select->from(['a' => 'users'])->columns(['id'], false);
+
+ $sql = $select->getSqlString(new TrustingSql92Platform());
+
+ self::assertStringContainsString('"id"', $sql);
+ self::assertStringNotContainsString('"a"."id"', $sql);
+ }
}
diff --git a/test/unit/Sql/SqlTest.php b/test/unit/Sql/SqlTest.php
index c7359a9c..c83b8a3e 100644
--- a/test/unit/Sql/SqlTest.php
+++ b/test/unit/Sql/SqlTest.php
@@ -10,11 +10,15 @@
use PhpDb\Adapter\Driver\DriverInterface;
use PhpDb\Adapter\Driver\ResultInterface;
use PhpDb\Adapter\Driver\StatementInterface;
+use PhpDb\Adapter\Platform\PlatformInterface;
use PhpDb\Sql\Delete;
use PhpDb\Sql\Exception\InvalidArgumentException;
+use PhpDb\Sql\Exception\RuntimeException;
use PhpDb\Sql\Insert;
+use PhpDb\Sql\Platform\PlatformDecoratorInterface;
use PhpDb\Sql\Select;
use PhpDb\Sql\Sql;
+use PhpDb\Sql\TableIdentifier;
use PhpDb\Sql\Update;
use PhpDbTest\TestAsset;
use PHPUnit\Framework\Attributes\CoversMethod;
@@ -158,4 +162,87 @@ public function testBuildSqlString(): void
$sqlString = $this->sql->buildSqlString($select);
self::assertEquals('SELECT "foo".* FROM "foo" WHERE "bar" = \'baz\'', $sqlString);
}
+
+ public function testSelectThrowsWhenTableConflicts(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'This Sql object is intended to work with only the table "foo" provided at construction time.'
+ );
+ $this->sql->select(new TableIdentifier('bar'));
+ }
+
+ public function testInsertThrowsWhenTableConflicts(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'This Sql object is intended to work with only the table "foo" provided at construction time.'
+ );
+ $this->sql->insert(new TableIdentifier('bar'));
+ }
+
+ public function testUpdateThrowsWhenTableConflicts(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'This Sql object is intended to work with only the table "foo" provided at construction time.'
+ );
+ $this->sql->update(new TableIdentifier('bar'));
+ }
+
+ public function testDeleteThrowsWhenTableConflicts(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'This Sql object is intended to work with only the table "foo" provided at construction time.'
+ );
+ $this->sql->delete(new TableIdentifier('bar'));
+ }
+
+ public function testGetSqlPlatformReturnsPlatformDecorator(): void
+ {
+ self::assertInstanceOf(PlatformDecoratorInterface::class, $this->sql->getSqlPlatform());
+ }
+
+ public function testPrepareStatementThrowsWhenPlatformNotPreparable(): void
+ {
+ $decorator = $this->createMock(PlatformDecoratorInterface::class);
+ $platform = $this->createMock(PlatformInterface::class);
+ $platform->method('getSqlPlatformDecorator')->willReturn($decorator);
+
+ $adapter = $this->getMockBuilder(Adapter::class)
+ ->setConstructorArgs([
+ $this->createMock(DriverInterface::class),
+ $platform,
+ ])
+ ->getMock();
+ $adapter->method('getPlatform')->willReturn($platform);
+
+ $sql = new Sql($adapter);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('does not implement PreparableSqlInterface');
+ $sql->prepareStatementForSqlObject($this->sql->select());
+ }
+
+ public function testBuildSqlStringThrowsWhenPlatformNotSqlInterface(): void
+ {
+ $decorator = $this->createMock(PlatformDecoratorInterface::class);
+ $platform = $this->createMock(PlatformInterface::class);
+ $platform->method('getSqlPlatformDecorator')->willReturn($decorator);
+
+ $adapter = $this->getMockBuilder(Adapter::class)
+ ->setConstructorArgs([
+ $this->createMock(DriverInterface::class),
+ $platform,
+ ])
+ ->getMock();
+ $adapter->method('getPlatform')->willReturn($platform);
+
+ $sql = new Sql($adapter);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('does not implement SqlInterface');
+ $sql->buildSqlString($this->sql->select());
+ }
}
diff --git a/test/unit/Sql/UpdateTest.php b/test/unit/Sql/UpdateTest.php
index 8c3dcc43..d653fb69 100644
--- a/test/unit/Sql/UpdateTest.php
+++ b/test/unit/Sql/UpdateTest.php
@@ -6,8 +6,10 @@
use Override;
use PhpDb\Adapter\Driver\DriverInterface;
+use PhpDb\Adapter\Driver\PdoDriverInterface;
use PhpDb\Adapter\Driver\StatementInterface;
use PhpDb\Adapter\ParameterContainer;
+use PhpDb\Adapter\StatementContainer;
use PhpDb\Sql\AbstractPreparableSql;
use PhpDb\Sql\Argument\Identifier;
use PhpDb\Sql\Argument\Value;
@@ -506,4 +508,52 @@ public function testWhereAcceptsExpressionInterface(): void
self::assertInstanceOf(Where::class, $where);
self::assertEquals(1, $where->count());
}
+
+ public function testProcessSetWithPdoDriverUsesDriverColumnQuoting(): void
+ {
+ $mockDriver = $this->getMockBuilder(PdoDriverInterface::class)->getMock();
+ $mockDriver->expects($this->any())->method('getPrepareType')->willReturn('positional');
+ $mockDriver->expects($this->any())
+ ->method('formatParameterName')
+ ->willReturnCallback(fn(string $name): string => ':' . $name);
+ $mockAdapter = $this->createMockAdapter($mockDriver);
+
+ $mockStatement = new StatementContainer();
+
+ $this->update->table('foo')
+ ->set(['bar' => 'baz', 'boo' => 'qux']);
+
+ $this->update->prepareStatement($mockAdapter, $mockStatement);
+
+ $sql = $mockStatement->getSql();
+ self::assertStringContainsString(':c_0', $sql);
+ self::assertStringContainsString(':c_1', $sql);
+ }
+
+ public function testCloneDeepCopiesSetWhereAndJoins(): void
+ {
+ $this->update->table('foo')
+ ->set(['bar' => 'baz'])
+ ->where('x = y')
+ ->join('other', 'foo.id = other.id');
+
+ $clone = clone $this->update;
+
+ $clone->set(['bar' => 'changed']);
+ $clone->where('z = w');
+ $clone->join('another', 'foo.id = another.id');
+
+ self::assertEquals(['bar' => 'baz'], $this->update->getRawState('set'));
+ self::assertEquals(['bar' => 'changed'], $clone->getRawState('set'));
+
+ self::assertNotSame(
+ $this->update->getRawState('where'),
+ $clone->getRawState('where')
+ );
+
+ self::assertNotSame(
+ $this->update->getRawState('joins'),
+ $clone->getRawState('joins')
+ );
+ }
}
diff --git a/test/unit/TableGateway/AbstractTableGatewayTest.php b/test/unit/TableGateway/AbstractTableGatewayTest.php
index a53bd1ce..ad845d76 100644
--- a/test/unit/TableGateway/AbstractTableGatewayTest.php
+++ b/test/unit/TableGateway/AbstractTableGatewayTest.php
@@ -21,8 +21,8 @@
use PhpDb\TableGateway\AbstractTableGateway;
use PhpDb\TableGateway\Exception\InvalidArgumentException;
use PhpDb\TableGateway\Exception\RuntimeException;
-use PhpDb\TableGateway\Feature\AbstractFeature;
use PhpDb\TableGateway\Feature\FeatureSet;
+use PhpDbTest\TableGateway\Feature\TestAsset\TestTableGatewayFeature;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\Attributes\RequiresPhp;
@@ -732,15 +732,8 @@ public function test__getWithFeatureSetMagicGet(): void
{
// @codingStandardsIgnoreEnd
// Create a custom feature that can handle magic get
- $feature = new class extends AbstractFeature {
- /**
- * @return array>
- */
- public function getMagicMethodSpecifications(): array
- {
- return ['get' => ['customProperty']];
- }
- };
+ $feature = new TestTableGatewayFeature();
+ $feature->magicMethodSpecs = ['get' => ['customProperty']];
// Create a FeatureSet mock that returns true for canCallMagicGet
$featureSet = $this->getMockBuilder(FeatureSet::class)
diff --git a/test/unit/TableGateway/Feature/EventFeatureTest.php b/test/unit/TableGateway/Feature/EventFeatureTest.php
index 4766ad5f..da86cf08 100644
--- a/test/unit/TableGateway/Feature/EventFeatureTest.php
+++ b/test/unit/TableGateway/Feature/EventFeatureTest.php
@@ -17,6 +17,7 @@
use PhpDb\TableGateway\Feature\EventFeature;
use PhpDb\TableGateway\Feature\EventFeatureEventsInterface;
use PhpDb\TableGateway\TableGateway;
+use PhpDbTest\TableGateway\Feature\TestAsset\TestTableGateway;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -300,12 +301,7 @@ public function testConstructorWithDefaults(): void
public function testPreInitializeAddsIdentifiersForCustomTableGatewayClass(): void
{
// Create a custom subclass of TableGateway (using anonymous class)
- $customTableGateway = new class extends TableGateway {
- public function __construct()
- {
- // Skip parent constructor
- }
- };
+ $customTableGateway = new TestTableGateway();
$eventManager = new EventManager();
$feature = new EventFeature($eventManager);
diff --git a/test/unit/TableGateway/Feature/FeatureSetTest.php b/test/unit/TableGateway/Feature/FeatureSetTest.php
index 93432061..95665c70 100644
--- a/test/unit/TableGateway/Feature/FeatureSetTest.php
+++ b/test/unit/TableGateway/Feature/FeatureSetTest.php
@@ -11,11 +11,11 @@
use PhpDb\Metadata\MetadataInterface;
use PhpDb\Metadata\Object\ConstraintObject;
use PhpDb\TableGateway\AbstractTableGateway;
-use PhpDb\TableGateway\Feature\AbstractFeature;
use PhpDb\TableGateway\Feature\FeatureSet;
use PhpDb\TableGateway\Feature\MasterSlaveFeature;
use PhpDb\TableGateway\Feature\MetadataFeature;
use PhpDb\TableGateway\Feature\SequenceFeature;
+use PhpDbTest\TableGateway\Feature\TestAsset\TestTableGatewayFeature;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
@@ -131,12 +131,7 @@ public function testCanCallMagicCallReturnsFalseWhenNoFeaturesHaveBeenAdded(): v
public function testCallMagicCallSucceedsForValidMethodOfAddedFeature(): void
{
- $feature = new class extends AbstractFeature {
- public function customMethod(array $args): string
- {
- return 'result: ' . ($args[0] ?? 'default');
- }
- };
+ $feature = new TestTableGatewayFeature();
$featureSet = new FeatureSet();
$featureSet->addFeature($feature);
@@ -255,22 +250,10 @@ public function testCallMagicCallReturnsNullWhenNoFeatureHasMethod(): void
public function testApplyHaltsWhenFeatureReturnsHalt(): void
{
- $feature1 = new class extends AbstractFeature {
- public bool $called = false;
- public function testMethod(): string
- {
- $this->called = true;
- return FeatureSet::APPLY_HALT;
- }
- };
-
- $feature2 = new class extends AbstractFeature {
- public bool $called = false;
- public function testMethod(): void
- {
- $this->called = true;
- }
- };
+ $feature1 = new TestTableGatewayFeature();
+ $feature1->returnValue = FeatureSet::APPLY_HALT;
+
+ $feature2 = new TestTableGatewayFeature();
$featureSet = new FeatureSet([$feature1, $feature2]);
$featureSet->apply('testMethod', []);
@@ -281,21 +264,8 @@ public function testMethod(): void
public function testApplyCallsAllFeaturesWhenNoHalt(): void
{
- $feature1 = new class extends AbstractFeature {
- public bool $called = false;
- public function testMethod(): void
- {
- $this->called = true;
- }
- };
-
- $feature2 = new class extends AbstractFeature {
- public bool $called = false;
- public function testMethod(): void
- {
- $this->called = true;
- }
- };
+ $feature1 = new TestTableGatewayFeature();
+ $feature2 = new TestTableGatewayFeature();
$featureSet = new FeatureSet([$feature1, $feature2]);
$featureSet->apply('testMethod', []);
@@ -306,17 +276,11 @@ public function testMethod(): void
public function testApplyPassesArgumentsToFeatures(): void
{
- $feature = new class extends AbstractFeature {
- public mixed $receivedArg;
- public function testMethod(string $arg): void
- {
- $this->receivedArg = $arg;
- }
- };
+ $feature = new TestTableGatewayFeature();
$featureSet = new FeatureSet([$feature]);
$featureSet->apply('testMethod', ['test value']);
- self::assertEquals('test value', $feature->receivedArg);
+ self::assertEquals(['test value'], $feature->receivedArgs);
}
}
diff --git a/test/unit/TableGateway/Feature/MetadataFeatureTest.php b/test/unit/TableGateway/Feature/MetadataFeatureTest.php
index 646a3097..66b929b8 100644
--- a/test/unit/TableGateway/Feature/MetadataFeatureTest.php
+++ b/test/unit/TableGateway/Feature/MetadataFeatureTest.php
@@ -12,7 +12,6 @@
use PhpDb\TableGateway\AbstractTableGateway;
use PhpDb\TableGateway\Exception\RuntimeException;
use PhpDb\TableGateway\Feature\MetadataFeature;
-use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\MockObject\Exception;
@@ -28,30 +27,6 @@ class MetadataFeatureTest extends TestCase
* @throws Exception
* @throws \Exception
*/
- #[Group('integration-test')]
- public function testPostInitialize(): void
- {
- $this->markTestSkipped('This is an integration test and requires a database connection.');
- /** @phpstan-ignore deadCode.unreachable */
- $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)
- ->onlyMethods([])
- ->getMock();
- $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock();
- $metadataMock->expects($this->any())->method('getColumnNames')->willReturn(['id', 'name']);
-
- $constraintObject = new ConstraintObject('id_pk', 'table');
- $constraintObject->setColumns(['id']);
- $constraintObject->setType('PRIMARY KEY');
-
- $metadataMock->expects($this->any())->method('getConstraints')->willReturn([$constraintObject]);
-
- $feature = new MetadataFeature($metadataMock);
- $feature->setTableGateway($tableGatewayMock);
- $feature->postInitialize();
-
- self::assertEquals(['id', 'name'], $tableGatewayMock->getColumns());
- }
-
/**
* @throws Exception
* @throws \Exception
diff --git a/test/unit/TableGateway/Feature/TestAsset/TestTableGateway.php b/test/unit/TableGateway/Feature/TestAsset/TestTableGateway.php
new file mode 100644
index 00000000..dd69b725
--- /dev/null
+++ b/test/unit/TableGateway/Feature/TestAsset/TestTableGateway.php
@@ -0,0 +1,14 @@
+ */
+ public array $receivedArgs = [];
+
+ public ?string $returnValue = null;
+
+ /** @var array> */
+ public array $magicMethodSpecs = [];
+
+ public function testMethod(mixed ...$args): mixed
+ {
+ $this->called = true;
+ $this->receivedArgs = $args;
+ return $this->returnValue;
+ }
+
+ public function customMethod(array $args): string
+ {
+ $this->called = true;
+ $this->receivedArgs = $args;
+ return 'result: ' . ($args[0] ?? 'default');
+ }
+
+ /** @return array> */
+ public function getMagicMethodSpecifications(): array
+ {
+ return $this->magicMethodSpecs;
+ }
+}
diff --git a/test/unit/TestAsset/TestSql92Platform.php b/test/unit/TestAsset/TestSql92Platform.php
new file mode 100644
index 00000000..8b66369d
--- /dev/null
+++ b/test/unit/TestAsset/TestSql92Platform.php
@@ -0,0 +1,17 @@
+