diff --git a/compose.yml.dist b/compose.yml.dist index c7b7cfd0b..adc63f204 100644 --- a/compose.yml.dist +++ b/compose.yml.dist @@ -61,9 +61,9 @@ services: environment: - SERVICES=s3 - DEBUG=1 - - DATA_DIR=/tmp/localstack/data + - DATA_DIR=/var/localstack/data volumes: - - "./var/localstack:/tmp/localstack" + - "./var/localstack:/var/localstack" otel-collector: image: otel/opentelemetry-collector-contrib:0.115.1 container_name: flow-php-otel-collector diff --git a/infection.json b/infection.json index e456d6d04..0ab7ecf51 100644 --- a/infection.json +++ b/infection.json @@ -1,145 +1,22 @@ { "source": { "directories": [ - "src/core/etl/src", - "src/lib/array-dot/src", - "src/lib/doctrine-dbal-bulk/src", - "src/lib/filesystem/src", - "src/lib/parquet/src", - "src/lib/telemetry/src", - "src/bridge/monolog/telemetry/src", - "src/bridge/psr7/telemetry/src", - "src/bridge/psr18/telemetry/src", - "src/bridge/symfony/http-foundation-telemetry/src", - "src/bridge/symfony/telemetry-bundle/src", - "src/bridge/telemetry/otlp/src" + "src/core/etl/src" ], "excludes": [ - "{.*/DSL.*}", - "Flow/ETL/Attribute", - "Flow/Calculator/Exception", - "Flow/Serializer/Exception", - "Flow/ETL/Exception", - "Flow/Types/Exception", - "Flow/Doctrine/Bulk/Exception", - "Flow/Filesystem/Exception", - "Flow/Parquet/Exception", - "Flow/Parquet/ThriftModel", - "Flow/ArrayDot/Exception", - "Flow/Telemetry/Exception", - "Flow/Telemetry/Provider/Void", - "Flow/Telemetry/Provider/Console", - "Flow/Bridge/Telemetry/OTLP/Exception", - "Flow/Bridge/Monolog/Telemetry/Exception", - "Flow/Bridge/Psr7/Telemetry/Exception", - "Flow/Bridge/Psr18/Telemetry/Exception", - "Flow/Bridge/Symfony/HttpFoundationTelemetry/Exception", - "Flow/Bridge/Symfony/TelemetryBundle/Exception" + "{.*/DSL.*}" ] }, "logs": { "text": "./var/infection/infection.log", "html": "./var/infection/infection.html", "summary": "./var/infection/infection_summary.log", - "debug": "./var/infection/infection_summary.log", "stryker": { "badge": "1.x" } }, "mutators": { - "@default": true, - "ArrayItem": { - "ignore": [ - "*::__serialize" - ] - }, - "ArrayItemRemoval": { - "ignore": [ - "*::__serialize", - "Flow\\ETL\\Adapter\\Logger\\Logger\\DumpLogger::log" - ] - }, - "Throw_": { - "ignore": [ - "Flow\\Doctrine\\Bulk\\QueryFactory\\DbalQueryFactory" - ] - }, - "DecrementInteger": { - "ignore": [ - "Flow\\ETL\\Extractor\\MemoryExtractor::extract", - "Flow\\Doctrine\\Bulk\\Exception\\RuntimeException::__construct", - "Flow\\Doctrine\\Bulk\\BulkData::toSqlParameters" - ] - }, - "IncrementInteger": { - "ignore": [ - "Flow\\ETL\\Extractor\\MemoryExtractor::extract", - "Flow\\Doctrine\\Bulk\\BulkData::toSqlParameters" - ] - }, - "Identical": { - "ignore": [ - "Flow\\Doctrine\\Bulk\\DbalPlatform" - ] - }, - "UnwrapArrayFilter": { - "ignore": [ - "Flow\\Doctrine\\Bulk\\BulkData" - ] - }, - "UnwrapRtrim": { - "ignore": [ - "Flow\\Calculator\\Calculator::*" - ] - }, - "LogicalAnd": { - "ignore": [ - "Flow\\ArrayComparison\\ArrayComparison::equals" - ] - }, - "LogicalOr": { - "ignore": [ - "Flow\\ArrayComparison\\ArrayComparison::equals" - ] - }, - "AssignCoalesce": { - "ignore": [ - "Flow\\ETL\\Config\\ConfigBuilder::build" - ] - }, - "Coalesce": { - "ignore": [ - "Flow\\ETL\\Cache\\Implementation\\FilesystemCache::__construct", - "Flow\\ETL\\Config\\Cache\\CacheConfigBuilder::build" - ] - }, - "CloneRemoval": { - "ignore": [ - "Flow\\ETL\\DataFrame::count", - "Flow\\ETL\\DataFrame::fetch", - "Flow\\ETL\\DataFrame::run", - "Flow\\ETL\\DataFrame::get*" - ] - }, - "MethodCallRemoval": { - "ignore": [ - "Flow\\ETL\\DataFrame::autoCast", - "Flow\\ETL\\DataFrame::aggregate", - "Flow\\ETL\\DataFrame::match" - ] - }, - "BitwiseAnd": { - "ignore": [ - "Flow\\Parquet\\Data\\*" - ] - }, - "Assignment": { - "ignore": [ - "Flow\\Parquet\\Writer\\*::addRow", - "Flow\\Parquet\\Writer\\*::addBytes", - "Flow\\Parquet\\Data\\*::*" - ] - } + "@default": true }, "bootstrap": "vendor/autoload.php", "phpUnit": { @@ -148,5 +25,10 @@ }, "tmpDir": "var/infection/cache", "minMsi": 30, - "minCoveredMsi": 70 + "minCoveredMsi": 70, + "staticAnalysisTool" : "phpstan", + "phpStan": { + "configDir": "tools/infection", + "customPath": "tools/phpstan/vendor/bin/phpstan" + } } diff --git a/phpstan.neon b/phpstan.neon index 8586b7cec..b00eb0a54 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -97,6 +97,7 @@ parameters: - src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Cache/* - src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/Doctrine/DBAL/V3/* - src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/Doctrine/DBAL/V4/* + - src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/* tmpDir: var/phpstan/cache diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a7c7828a0..3d4383a31 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -30,8 +30,7 @@ - + @@ -229,15 +228,15 @@ - src/adapter/**/src - src/bridge/**/**/src - src/core/**/src - src/cli/src - src/lib/**/src + src/adapter/**/src + src/bridge/**/**/src + src/core/**/src + src/cli/src + src/lib/**/src - src/lib/parquet/src/Flow/Parquet/Thrift - src/lib/postgresql/src/Flow/PostgreSql/Protobuf + src/lib/parquet/src/Flow/Parquet/Thrift + src/lib/postgresql/src/Flow/PostgreSql/Protobuf src/core/etl/src/Flow/ETL/DSL/functions.php src/lib/postgresql/src/Flow/PostgreSql/DSL/functions.php src/functions.php diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php index b8f9414a0..7c5dffe07 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php @@ -70,13 +70,13 @@ public function addTestExtensionConfig(string $extension, array $config) : void #[\Override] public function getCacheDir() : string { - return __DIR__ . '/../../../../../../../var/flow_telemetry_bundle_test/' . $this->environment . '/cache'; + return __DIR__ . '/../../../../../../../var/flow_telemetry_bundle_test/' . $this->environment . '/' . $this->testId . '/cache'; } #[\Override] public function getLogDir() : string { - return __DIR__ . '/../../../../../../../var/flow_telemetry_bundle_test/' . $this->environment . '/log'; + return __DIR__ . '/../../../../../../../var/flow_telemetry_bundle_test/' . $this->environment . '/' . $this->testId . '/log'; } #[\Override] diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/TracingConnectionTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/TracingConnectionTest.php index eb5e32da0..080928b5d 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/TracingConnectionTest.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/TracingConnectionTest.php @@ -5,7 +5,7 @@ namespace Flow\Bridge\Symfony\TelemetryBundle\Tests\Unit\Instrumentation\Doctrine\DBAL; use Doctrine\DBAL\Driver\{Connection as ConnectionInterface, Result, Statement as DriverStatement}; -use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\{ParameterType, VersionAwarePlatformDriver}; use Flow\Bridge\Symfony\TelemetryBundle\Instrumentation\Doctrine\DBAL\V4\TracingConnection; use Flow\Telemetry\Context\MemoryContextStorage; use Flow\Telemetry\Logger\LoggerProvider; @@ -21,6 +21,13 @@ #[CoversClass(TracingConnection::class)] final class TracingConnectionTest extends TestCase { + protected function setUp() : void + { + if (\interface_exists(VersionAwarePlatformDriver::class)) { + self::markTestSkipped('Test requires Doctrine DBAL 4.x'); + } + } + public function test_prepare_uses_truncation() : void { $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); @@ -328,8 +335,7 @@ public function rowCount() : int }; } - /** @phpstan-ignore missingType.parameter, missingType.parameter */ - public function quote($value, $type = ParameterType::STRING) : string + public function quote(string $value) : string { return "'{$value}'"; } diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/TracingDriverTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/TracingDriverTest.php index 337c59ec2..6428862c1 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/TracingDriverTest.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/TracingDriverTest.php @@ -6,7 +6,7 @@ use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Driver\{Connection, Result, Statement}; -use Doctrine\DBAL\{Driver, ServerVersionProvider}; +use Doctrine\DBAL\{Driver, ServerVersionProvider, VersionAwarePlatformDriver}; use Doctrine\DBAL\Platforms\{AbstractPlatform, DB2Platform, MariaDBPlatform, MySQL80Platform, OraclePlatform, PostgreSQLPlatform, SQLServerPlatform, SQLitePlatform}; use Flow\Bridge\Symfony\TelemetryBundle\Instrumentation\Doctrine\DBAL\V4\TracingDriver; use Flow\Telemetry\Context\MemoryContextStorage; @@ -23,6 +23,13 @@ #[CoversClass(TracingDriver::class)] final class TracingDriverTest extends TestCase { + protected function setUp() : void + { + if (\interface_exists(VersionAwarePlatformDriver::class)) { + self::markTestSkipped('Test requires Doctrine DBAL 4.x'); + } + } + public function test_get_semantic_db_system_defaults_to_other_sql() : void { $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/V3/TracingConnectionTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/V3/TracingConnectionTest.php new file mode 100644 index 000000000..f38ab4428 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/V3/TracingConnectionTest.php @@ -0,0 +1,375 @@ +createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: true, maxSqlLength: 15); + + $sql = 'INSERT INTO users (name, email) VALUES (?, ?)'; + $tracing->prepare($sql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('INSERT INTO use...', $spans[0]->attributes()['db.query.text']); + } + + public function test_query_uses_truncation() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: true, maxSqlLength: 10); + + $sql = 'SELECT * FROM users WHERE id = 1'; + $tracing->query($sql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('SELECT * F...', $spans[0]->attributes()['db.query.text']); + } + + public function test_sql_not_logged_when_log_sql_disabled() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: false, maxSqlLength: 100); + + $sql = 'SELECT * FROM users'; + $tracing->exec($sql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertArrayNotHasKey('db.query.text', $spans[0]->attributes()); + } + + public function test_truncate_sql_exact_boundary_case() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: true, maxSqlLength: 10); + + $sql = '1234567890'; + $tracing->exec($sql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame($sql, $spans[0]->attributes()['db.query.text']); + } + + public function test_truncate_sql_handles_multibyte_characters() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: true, maxSqlLength: 15); + + $sql = "SELECT * FROM users WHERE name = '日本語テスト'"; + $tracing->exec($sql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + + $truncated = $spans[0]->attributes()['db.query.text']; + self::assertSame('SELECT * FROM u...', $truncated); + self::assertSame(18, \mb_strlen($truncated)); + } + + public function test_truncate_sql_returns_full_sql_when_max_length_negative() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: true, maxSqlLength: -1); + + $longSql = \str_repeat('SELECT * FROM users; ', 100); + $tracing->exec($longSql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame($longSql, $spans[0]->attributes()['db.query.text']); + } + + public function test_truncate_sql_returns_full_sql_when_max_length_zero() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: true, maxSqlLength: 0); + + $longSql = \str_repeat('SELECT * FROM users; ', 100); + $tracing->exec($longSql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame($longSql, $spans[0]->attributes()['db.query.text']); + } + + public function test_truncate_sql_returns_sql_when_shorter_than_limit() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: true, maxSqlLength: 100); + + $sql = 'SELECT * FROM users WHERE id = 1'; + $tracing->exec($sql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame($sql, $spans[0]->attributes()['db.query.text']); + } + + public function test_truncate_sql_truncates_and_appends_ellipsis() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $connection = $this->createMockConnection(); + $tracing = new TracingConnection($connection, $telemetry, logSql: true, maxSqlLength: 20); + + $sql = 'SELECT * FROM users WHERE id = 1 AND status = active'; + $tracing->exec($sql); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('SELECT * FROM users ...', $spans[0]->attributes()['db.query.text']); + } + + private function createMockConnection() : ConnectionInterface + { + return new class implements ConnectionInterface { + public function beginTransaction() : bool + { + return true; + } + + public function commit() : bool + { + return true; + } + + public function exec(string $sql) : int + { + return 0; + } + + public function getNativeConnection() : object + { + return new \stdClass(); + } + + public function getServerVersion() : string + { + return '8.0.0'; + } + + /** @phpstan-ignore missingType.parameter */ + public function lastInsertId($name = null) : string|int|false + { + return 0; + } + + public function prepare(string $sql) : DriverStatement + { + return new class implements DriverStatement { + /** @phpstan-ignore missingType.parameter */ + public function bindValue($param, $value, $type = ParameterType::STRING) : bool + { + return true; + } + + /** @phpstan-ignore missingType.parameter */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) : bool + { + return true; + } + + /** @phpstan-ignore missingType.parameter */ + public function execute($params = null) : Result + { + return new class implements Result { + public function columnCount() : int + { + return 0; + } + + /** @return list> */ + public function fetchAllAssociative() : array + { + return []; + } + + /** @return array */ + public function fetchAllKeyValue() : array + { + return []; + } + + /** @return list> */ + public function fetchAllNumeric() : array + { + return []; + } + + /** @return array|false */ + public function fetchAssociative() : array|false + { + return false; + } + + /** @return list */ + public function fetchFirstColumn() : array + { + return []; + } + + /** @return false|list */ + public function fetchNumeric() : array|false + { + return false; + } + + public function fetchOne() : mixed + { + return false; + } + + public function free() : void + { + } + + public function rowCount() : int + { + return 0; + } + }; + } + }; + } + + public function query(string $sql) : Result + { + return new class implements Result { + public function columnCount() : int + { + return 0; + } + + /** @return list> */ + public function fetchAllAssociative() : array + { + return []; + } + + /** @return array */ + public function fetchAllKeyValue() : array + { + return []; + } + + /** @return list> */ + public function fetchAllNumeric() : array + { + return []; + } + + /** @return array|false */ + public function fetchAssociative() : array|false + { + return false; + } + + /** @return list */ + public function fetchFirstColumn() : array + { + return []; + } + + /** @return false|list */ + public function fetchNumeric() : array|false + { + return false; + } + + public function fetchOne() : mixed + { + return false; + } + + public function free() : void + { + } + + public function rowCount() : int + { + return 0; + } + }; + } + + /** @phpstan-ignore missingType.parameter, missingType.parameter */ + public function quote($value, $type = ParameterType::STRING) : mixed + { + return "'{$value}'"; + } + + public function rollBack() : bool + { + return true; + } + }; + } + + private function createTelemetry(MemorySpanProcessor $spanProcessor) : Telemetry + { + $clock = new SystemClock(); + $contextStorage = new MemoryContextStorage(); + + return new Telemetry( + Resource::create(['service.name' => 'test']), + new TracerProvider($spanProcessor, $clock, $contextStorage), + new MeterProvider(new VoidMetricProcessor(), $clock), + new LoggerProvider(new VoidLogProcessor(), $clock, $contextStorage), + ); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/V3/TracingDriverTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/V3/TracingDriverTest.php new file mode 100644 index 000000000..2636c7890 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Doctrine/DBAL/V3/TracingDriverTest.php @@ -0,0 +1,321 @@ +createTelemetry($spanProcessor); + + $platform = $this->createMock(AbstractPlatform::class); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('other_sql', $spans[0]->attributes()['db.system']); + } + + public function test_get_semantic_db_system_detects_db2() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new DB2Platform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('db2', $spans[0]->attributes()['db.system']); + } + + public function test_get_semantic_db_system_detects_mariadb_as_mysql() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new MariaDBPlatform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('mysql', $spans[0]->attributes()['db.system']); + } + + public function test_get_semantic_db_system_detects_mssql() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new SQLServerPlatform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('mssql', $spans[0]->attributes()['db.system']); + } + + public function test_get_semantic_db_system_detects_mysql() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new MySQL80Platform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('mysql', $spans[0]->attributes()['db.system']); + } + + public function test_get_semantic_db_system_detects_oracle() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new OraclePlatform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('oracle', $spans[0]->attributes()['db.system']); + } + + public function test_get_semantic_db_system_detects_postgresql() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new PostgreSQLPlatform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('postgresql', $spans[0]->attributes()['db.system']); + } + + public function test_get_semantic_db_system_detects_sqlite() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new SqlitePlatform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('sqlite', $spans[0]->attributes()['db.system']); + } + + public function test_span_defaults_db_namespace_to_default() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new PostgreSQLPlatform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('default', $spans[0]->attributes()['db.namespace']); + } + + public function test_span_includes_connection_name() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new PostgreSQLPlatform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'analytics', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect([]); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('analytics', $spans[0]->attributes()['db.connection.name']); + } + + public function test_span_includes_db_namespace_from_params() : void + { + $spanProcessor = new MemorySpanProcessor(new MemorySpanExporter()); + $telemetry = $this->createTelemetry($spanProcessor); + + $platform = new PostgreSQLPlatform(); + $driver = $this->createMockDriverWithPlatform($platform); + + $tracingDriver = new TracingDriver($telemetry, $driver, 'default', logSql: true, maxSqlLength: 100); + + $tracingDriver->connect(['dbname' => 'my_database']); + + $spans = $spanProcessor->endedSpans(); + self::assertCount(1, $spans); + self::assertSame('my_database', $spans[0]->attributes()['db.namespace']); + } + + private function createMockDriverWithPlatform(AbstractPlatform $platform) : Driver + { + return new readonly class($platform) implements VersionAwarePlatformDriver { + public function __construct(private AbstractPlatform $platform) + { + } + + public function connect(array $params) : Connection + { + return new class implements Connection { + public function beginTransaction() : bool + { + return true; + } + + public function commit() : bool + { + return true; + } + + public function exec(string $sql) : int + { + return 0; + } + + public function getNativeConnection() : object + { + return new \stdClass(); + } + + public function getServerVersion() : string + { + return '1.0.0'; + } + + /** @phpstan-ignore missingType.parameter */ + public function lastInsertId($name = null) : string|int|false + { + return 0; + } + + public function prepare(string $sql) : Statement + { + throw new \RuntimeException('Not implemented'); + } + + public function query(string $sql) : Result + { + throw new \RuntimeException('Not implemented'); + } + + /** @phpstan-ignore missingType.parameter, missingType.parameter */ + public function quote($value, $type = ParameterType::STRING) : mixed + { + return "'{$value}'"; + } + + public function rollBack() : bool + { + return true; + } + }; + } + + public function createDatabasePlatformForVersion($version) : AbstractPlatform + { + return $this->platform; + } + + public function getDatabasePlatform() : AbstractPlatform + { + return $this->platform; + } + + /** @phpstan-ignore missingType.parameter */ + public function getSchemaManager(\Doctrine\DBAL\Connection $conn, AbstractPlatform $platform) : AbstractSchemaManager + { + throw new \RuntimeException('Not implemented'); + } + + public function getExceptionConverter() : ExceptionConverter + { + throw new \RuntimeException('Not implemented'); + } + }; + } + + private function createTelemetry(MemorySpanProcessor $spanProcessor) : Telemetry + { + $clock = new SystemClock(); + $contextStorage = new MemoryContextStorage(); + + return new Telemetry( + Resource::create(['service.name' => 'test']), + new TracerProvider($spanProcessor, $clock, $contextStorage), + new MeterProvider(new VoidMetricProcessor(), $clock), + new LoggerProvider(new VoidLogProcessor(), $clock, $contextStorage), + ); + } +} diff --git a/tools/cs-fixer/composer.lock b/tools/cs-fixer/composer.lock index 283456759..b8e94b93a 100644 --- a/tools/cs-fixer/composer.lock +++ b/tools/cs-fixer/composer.lock @@ -403,16 +403,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.93.1", + "version": "v3.94.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a" + "reference": "883b20fb38c7866de9844ab6d0a205c423bde2d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b3546ab487c0762c39f308dc1ec0ea2c461fc21a", - "reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/883b20fb38c7866de9844ab6d0a205c423bde2d4", + "reference": "883b20fb38c7866de9844ab6d0a205c423bde2d4", "shasum": "" }, "require": { @@ -429,7 +429,7 @@ "react/event-loop": "^1.5", "react/socket": "^1.16", "react/stream": "^1.4", - "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0", "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", @@ -443,18 +443,18 @@ "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.7", - "infection/infection": "^0.32", - "justinrainbow/json-schema": "^6.6", + "facile-it/paraunit": "^1.3.1 || ^2.7.1", + "infection/infection": "^0.32.3", + "justinrainbow/json-schema": "^6.6.4", "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.9", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.48", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51", "symfony/polyfill-php85": "^1.33", - "symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0", - "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0" + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -495,7 +495,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.0" }, "funding": [ { @@ -503,7 +503,7 @@ "type": "github" } ], - "time": "2026-01-28T23:50:50+00:00" + "time": "2026-02-11T16:44:33+00:00" }, { "name": "psr/container", diff --git a/tools/infection/phpstan.neon b/tools/infection/phpstan.neon new file mode 100644 index 000000000..a7083fa56 --- /dev/null +++ b/tools/infection/phpstan.neon @@ -0,0 +1,25 @@ +parameters: + level: 9 + treatPhpDocTypesAsCertain: false + bootstrapFiles: + - ../../tools/phpunit/vendor/autoload.php + - ../../vendor/autoload.php + paths: + - ../../src/core/etl/src + + excludePaths: + - ../../src/core/etl/src/Flow/ETL/Formatter/ASCII/ASCIITable.php + - ../../src/core/etl/src/Flow/ETL/Sort/ExternalSort/RowsMinHeap.php + + tmpDir: ../../var/infection/phpstan-cache + + ignoreErrors: + - + message: '#Dom\\(CharacterData|HTMLDocument|HTMLElement|Element)#i' + identifier: class.notFound + +services: + - + class: Flow\Types\PHPStan\StructureTypeReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension diff --git a/tools/infection/phpunit.xml b/tools/infection/phpunit.xml index 61a2f97dc..b150360b5 100644 --- a/tools/infection/phpunit.xml +++ b/tools/infection/phpunit.xml @@ -11,51 +11,14 @@ ../../src/core/etl/tests/Flow/ETL/Tests/Unit - ../../src/lib/array-dot/tests/Flow/ArrayDot/Tests/Unit - ../../src/lib/dremel/tests/Flow/Dremel/Tests/Unit - ../../src/lib/types/tests/Flow/Types/Tests/Unit - ../../src/lib/doctrine-dbal-bulk/tests/Flow/Doctrine/Bulk/Tests/Unit - ../../src/lib/filesystem/tests/Flow/Filesystem/Tests/Unit - ../../src/lib/parquet/tests/Flow/Parquet/Tests/Unit - ../../src/lib/telemetry/tests/Flow/Telemetry/Tests/Unit - ../../src/bridge/monolog/telemetry/tests/Flow/Bridge/Monolog/Telemetry/Tests/Unit - ../../src/bridge/symfony/http-foundation-telemetry/tests/Flow/Bridge/Symfony/HttpFoundationTelemetry/Tests/Unit - ../../src/bridge/psr7/telemetry/tests/Flow/Bridge/Psr7/Telemetry/Tests/Unit - ../../src/bridge/psr18/telemetry/tests/Flow/Bridge/Psr18/Telemetry/Tests/Unit - ../../src/bridge/telemetry/otlp/tests/Flow/Bridge/Telemetry/OTLP/Tests/Unit - ../../src/core/etl/src - ../../src/lib/array-dot/src - ../../src/lib/dremel/src - ../../src/lib/types/src - ../../src/lib/doctrine-dbal-bulk/src - ../../src/lib/filesystem/src - ../../src/lib/parquet/src - ../../src/lib/telemetry/src - ../../src/bridge/monolog/telemetry/src - ../../src/bridge/symfony/http-foundation-telemetry/src - ../../src/bridge/psr7/telemetry/src - ../../src/bridge/psr18/telemetry/src - ../../src/bridge/telemetry/otlp/src + ../../src/core/etl/src/Flow/ETL - ../../src/lib/parquet/src/Flow/Parquet/Thrift - ../../src/core/etl/src/Flow/ETL/DSL - ../../src/lib/array-dot/src/Flow/ArrayDot/DSL - ../../src/lib/dremel/src/Flow/Dremel/DSL - ../../src/lib/types/src/Flow/Types/DSL - ../../src/lib/doctrine-dbal-bulk/src/Flow/Doctrine/Bulk/DSL - ../../src/lib/filesystem/src/Flow/Filesystem/DSL - ../../src/lib/parquet/src/Flow/Parquet/DSL - ../../src/lib/telemetry/src/Flow/Telemetry/DSL - ../../src/bridge/monolog/telemetry/src/Flow/Bridge/Monolog/Telemetry/DSL - ../../src/bridge/symfony/http-foundation-telemetry/src/Flow/Bridge/Symfony/HttpFoundationTelemetry/DSL - ../../src/bridge/psr7/telemetry/src/Flow/Bridge/Psr7/Telemetry/DSL - ../../src/bridge/psr18/telemetry/src/Flow/Bridge/Psr18/Telemetry/DSL - ../../src/bridge/telemetry/otlp/src/Flow/Bridge/Telemetry/OTLP/DSL + ../../src/core/etl/src/Flow/ETL/DSL diff --git a/tools/rector/composer.lock b/tools/rector/composer.lock index 75930daf2..18c1ed2ae 100644 --- a/tools/rector/composer.lock +++ b/tools/rector/composer.lock @@ -9,11 +9,11 @@ "packages-dev": [ { "name": "phpstan/phpstan", - "version": "2.1.38", + "version": "2.1.39", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", - "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224", + "reference": "c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224", "shasum": "" }, "require": { @@ -58,7 +58,7 @@ "type": "github" } ], - "time": "2026-01-30T17:12:46+00:00" + "time": "2026-02-11T14:48:56+00:00" }, { "name": "rector/rector",