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",