@@ -560,12 +560,14 @@ public function testValidatePoolAcceptsAllPoolBehaviorKeys(): void
560560 'dead_cache_ttl_seconds ' => 60 ,
561561 'replica_selector ' => 'random ' ,
562562 'max_connection_attempts ' => 6 ,
563+ 'query_timeout_ms ' => 5000 ,
563564 ]);
564565
565566 $ this ->assertFalse ($ pool ->healthCheck );
566567 $ this ->assertSame (60 , $ pool ->deadCacheTtlSeconds );
567568 $ this ->assertSame ('random ' , $ pool ->replicaSelector );
568569 $ this ->assertSame (6 , $ pool ->maxConnectionAttempts );
570+ $ this ->assertSame (5000 , $ pool ->queryTimeoutMs );
569571 }
570572
571573 public function testValidatePoolDefaultsMaxConnectionAttemptsToReplicaCountPlusOne (): void
@@ -588,15 +590,15 @@ public function testValidatePoolRejectsUnknownPoolKey(): void
588590 {
589591 try {
590592 ConnectionConfigResolver::validatePool ('mydb ' , [
591- 'driver ' => 'mysql ' ,
592- 'host ' => 'primary.example.com ' ,
593- 'database ' => 'app ' ,
594- 'query_timeout_ms ' => 5000 ,
593+ 'driver ' => 'mysql ' ,
594+ 'host ' => 'primary.example.com ' ,
595+ 'database ' => 'app ' ,
596+ 'persistent ' => true ,
595597 ]);
596598 $ this ->fail ('Expected InvalidConfigException ' );
597599 } catch (InvalidConfigException $ e ) {
598600 $ this ->assertSame (
599- 'Connection [mydb]: unsupported config key "query_timeout_ms ". ' ,
601+ 'Connection [mydb]: unsupported config key "persistent ". ' ,
600602 $ e ->getMessage (),
601603 );
602604 }
@@ -886,6 +888,7 @@ public function testValidatePoolDefaultsLoggingKeysWhenOmitted(): void
886888 $ this ->assertTrue ($ pool ->logBindings );
887889 $ this ->assertFalse ($ pool ->logAllQueries );
888890 $ this ->assertNull ($ pool ->slowQueryThresholdMs );
891+ $ this ->assertNull ($ pool ->queryTimeoutMs );
889892 }
890893
891894 public function testValidatePoolAcceptsExplicitLogBindings (): void
@@ -1042,4 +1045,92 @@ public function testValidatePoolAcceptsExplicitNullMaxConnectionAttempts(): void
10421045
10431046 $ this ->assertSame (3 , $ pool ->maxConnectionAttempts );
10441047 }
1048+
1049+ /**
1050+ * @return array<string, array{0: int|null, 1: int|null}>
1051+ */
1052+ public static function validQueryTimeoutMsProvider (): array
1053+ {
1054+ return [
1055+ 'positive int ' => [5000 , 5000 ],
1056+ 'minimum 1 ' => [1 , 1 ],
1057+ 'explicit null ' => [null , null ],
1058+ ];
1059+ }
1060+
1061+ #[DataProvider('validQueryTimeoutMsProvider ' )]
1062+ public function testValidatePoolAcceptsValidQueryTimeoutMs (?int $ input , ?int $ expected ): void
1063+ {
1064+ // Valid inputs: positive int (typical), 1 (boundary inclusion guarding
1065+ // a `> 1` regression), and explicit null (the "off" sentinel shared
1066+ // with slow_query_threshold_ms / dead_cache_ttl_seconds).
1067+ $ pool = ConnectionConfigResolver::validatePool ('mydb ' , [
1068+ 'driver ' => 'mysql ' ,
1069+ 'host ' => 'primary.example.com ' ,
1070+ 'database ' => 'app ' ,
1071+ 'query_timeout_ms ' => $ input ,
1072+ ]);
1073+
1074+ $ this ->assertSame ($ expected , $ pool ->queryTimeoutMs );
1075+ }
1076+
1077+ /**
1078+ * @return array<string, array{0: mixed, 1: string}>
1079+ */
1080+ public static function invalidQueryTimeoutMsProvider (): array
1081+ {
1082+ return [
1083+ 'non-int string ' => ['5000 ' , 'must be an integer ' ],
1084+ 'float ' => [1.5 , 'must be an integer ' ],
1085+ 'bool true ' => [true , 'must be an integer ' ],
1086+ 'zero ' => [0 , 'must be >= 1, got 0 ' ],
1087+ 'negative ' => [-1 , 'must be >= 1, got -1 ' ],
1088+ ];
1089+ }
1090+
1091+ #[DataProvider('invalidQueryTimeoutMsProvider ' )]
1092+ public function testValidatePoolRejectsInvalidQueryTimeoutMs (mixed $ value , string $ expectedSuffix ): void
1093+ {
1094+ // The two error-message templates correspond to the two rejection
1095+ // branches in extractOptionalPositiveInt: type check (`!is_int`) and
1096+ // range check (`< 1`). Bool/float are exercised explicitly so silent
1097+ // coercion regressions are caught.
1098+ try {
1099+ ConnectionConfigResolver::validatePool ('mydb ' , [
1100+ 'driver ' => 'mysql ' ,
1101+ 'host ' => 'primary.example.com ' ,
1102+ 'database ' => 'app ' ,
1103+ 'query_timeout_ms ' => $ value ,
1104+ ]);
1105+ $ this ->fail ('Expected InvalidConfigException ' );
1106+ } catch (InvalidConfigException $ e ) {
1107+ $ this ->assertSame (
1108+ 'Connection [mydb]: config key "query_timeout_ms" ' . $ expectedSuffix . '. ' ,
1109+ $ e ->getMessage (),
1110+ );
1111+ }
1112+ }
1113+
1114+ public function testValidatePoolRejectsQueryTimeoutMsInsideReplica (): void
1115+ {
1116+ // query_timeout_ms is pool-level: it must be set on the pool itself,
1117+ // never inside a read[] entry. Same protection as the health_check
1118+ // regression in testValidatePoolRejectsPoolOnlyKeyInsideReplica.
1119+ try {
1120+ ConnectionConfigResolver::validatePool ('mydb ' , [
1121+ 'driver ' => 'mysql ' ,
1122+ 'host ' => 'primary.example.com ' ,
1123+ 'database ' => 'app ' ,
1124+ 'read ' => [
1125+ ['host ' => 'replica-1.example.com ' , 'query_timeout_ms ' => 5000 ],
1126+ ],
1127+ ]);
1128+ $ this ->fail ('Expected InvalidConfigException ' );
1129+ } catch (InvalidConfigException $ e ) {
1130+ $ this ->assertSame (
1131+ 'Connection [mydb]: "read[0]" has unsupported key "query_timeout_ms". Pool-level keys must be set on the pool itself, not inside read[]. ' ,
1132+ $ e ->getMessage (),
1133+ );
1134+ }
1135+ }
10451136}
0 commit comments