@@ -7,8 +7,234 @@ class WP_PDO_MySQL_On_SQLite_PDO_API_Tests extends TestCase {
77 private $ driver ;
88
99 public function setUp (): void {
10- $ connection = new WP_SQLite_Connection ( array ( 'path ' => ':memory: ' ) );
11- $ this ->driver = new WP_PDO_MySQL_On_SQLite ( $ connection , 'wp ' );
10+ $ this ->driver = new WP_PDO_MySQL_On_SQLite ( 'mysql-on-sqlite:path=:memory:;dbname=wp; ' );
11+
12+ // Run all tests with stringified fetch mode results, so we can use
13+ // assertions that are consistent across all tested PHP versions.
14+ // The "PDO::ATTR_STRINGIFY_FETCHES" mode is tested separately.
15+ $ this ->driver ->setAttribute ( PDO ::ATTR_STRINGIFY_FETCHES , true );
16+ }
17+
18+ public function test_connection (): void {
19+ $ driver = new WP_PDO_MySQL_On_SQLite ( 'mysql-on-sqlite:path=:memory:;dbname=WordPress; ' );
20+ $ this ->assertInstanceOf ( PDO ::class, $ driver );
21+ }
22+
23+ public function test_dsn_parsing (): void {
24+ // Standard DSN.
25+ $ driver = new WP_PDO_MySQL_On_SQLite ( 'mysql-on-sqlite:path=:memory:;dbname=wp ' );
26+ $ this ->assertSame ( 'wp ' , $ driver ->query ( 'SELECT DATABASE() ' )->fetch ()[0 ] );
27+
28+ // DSN with trailing semicolon.
29+ $ driver = new WP_PDO_MySQL_On_SQLite ( 'mysql-on-sqlite:path=:memory:;dbname=wp; ' );
30+ $ this ->assertSame ( 'wp ' , $ driver ->query ( 'SELECT DATABASE() ' )->fetch ()[0 ] );
31+
32+ // DSN with whitespace before argument names.
33+ $ driver = new WP_PDO_MySQL_On_SQLite ( "mysql-on-sqlite: path=:memory:; \n\r\t\v\fdbname=wp " );
34+ $ this ->assertSame ( 'wp ' , $ driver ->query ( 'SELECT DATABASE() ' )->fetch ()[0 ] );
35+
36+ // DSN with whitespace in the database name.
37+ $ driver = new WP_PDO_MySQL_On_SQLite ( 'mysql-on-sqlite:path=:memory:;dbname= w p ' );
38+ $ this ->assertSame ( ' w p ' , $ driver ->query ( 'SELECT DATABASE() ' )->fetch ()[0 ] );
39+
40+ // DSN with semicolon in the database name.
41+ $ driver = new WP_PDO_MySQL_On_SQLite ( 'mysql-on-sqlite:path=:memory:;dbname=wp;dbname=w;;p; ' );
42+ $ this ->assertSame ( 'w;p ' , $ driver ->query ( 'SELECT DATABASE() ' )->fetch ()[0 ] );
43+
44+ // DSN with semicolon in the database name and a terminating semicolon.
45+ $ driver = new WP_PDO_MySQL_On_SQLite ( 'mysql-on-sqlite:path=:memory:;dbname=w;;;p ' );
46+ $ this ->assertSame ( 'w; ' , $ driver ->query ( 'SELECT DATABASE() ' )->fetch ()[0 ] );
47+
48+ // DSN with two semicolons in the database name.
49+ $ driver = new WP_PDO_MySQL_On_SQLite ( 'mysql-on-sqlite:path=:memory:;dbname=w;;;;p ' );
50+ $ this ->assertSame ( 'w;;p ' , $ driver ->query ( 'SELECT DATABASE() ' )->fetch ()[0 ] );
51+
52+ // DSN with a "\0" byte (always terminates the DSN string).
53+ $ driver = new WP_PDO_MySQL_On_SQLite ( "mysql-on-sqlite:path=:memory:;dbname=w \0p; " );
54+ $ this ->assertSame ( 'w ' , $ driver ->query ( 'SELECT DATABASE() ' )->fetch ()[0 ] );
55+ }
56+
57+ public function test_query (): void {
58+ $ result = $ this ->driver ->query ( "SELECT 1, 'abc' " );
59+ $ this ->assertInstanceOf ( PDOStatement::class, $ result );
60+ if ( PHP_VERSION_ID < 80000 ) {
61+ $ this ->assertSame (
62+ array (
63+ 1 => '1 ' ,
64+ 2 => '1 ' ,
65+ 'abc ' => 'abc ' ,
66+ 3 => 'abc ' ,
67+ ),
68+ $ result ->fetch ()
69+ );
70+ } else {
71+ $ this ->assertSame (
72+ array (
73+ 1 => '1 ' ,
74+ 0 => '1 ' ,
75+ 'abc ' => 'abc ' ,
76+ ),
77+ $ result ->fetch ()
78+ );
79+ }
80+ }
81+
82+ /**
83+ * @dataProvider data_pdo_fetch_methods
84+ */
85+ public function test_query_with_fetch_mode ( $ query , $ mode , $ expected ): void {
86+ $ stmt = $ this ->driver ->query ( $ query , $ mode );
87+ $ result = $ stmt ->fetch ();
88+
89+ if ( is_object ( $ expected ) ) {
90+ $ this ->assertInstanceOf ( get_class ( $ expected ), $ result );
91+ $ this ->assertSame ( (array ) $ expected , (array ) $ result );
92+ } elseif ( PDO ::FETCH_NAMED === $ mode ) {
93+ // PDO::FETCH_NAMED returns all array keys as strings, even numeric
94+ // ones. This is not possible in plain PHP and might be a PDO bug.
95+ $ this ->assertSame ( array_map ( 'strval ' , array_keys ( $ expected ) ), array_keys ( $ result ) );
96+ $ this ->assertSame ( array_values ( $ expected ), array_values ( $ result ) );
97+ } else {
98+ $ this ->assertSame ( $ expected , $ result );
99+ }
100+
101+ $ this ->assertFalse ( $ stmt ->fetch () );
102+ }
103+
104+ public function test_query_fetch_mode_not_set (): void {
105+ $ result = $ this ->driver ->query ( 'SELECT 1 ' );
106+ if ( PHP_VERSION_ID < 80000 ) {
107+ $ this ->assertSame (
108+ array (
109+ 1 => '1 ' ,
110+ 2 => '1 ' ,
111+ ),
112+ $ result ->fetch ()
113+ );
114+ } else {
115+ $ this ->assertSame (
116+ array (
117+ 1 => '1 ' ,
118+ 0 => '1 ' ,
119+ ),
120+ $ result ->fetch ()
121+ );
122+ }
123+ $ this ->assertFalse ( $ result ->fetch () );
124+ }
125+
126+ public function test_query_fetch_mode_invalid_arg_count (): void {
127+ $ this ->expectException ( ArgumentCountError::class );
128+ $ this ->expectExceptionMessage ( 'PDO::query() expects exactly 2 arguments for the fetch mode provided, 3 given ' );
129+ $ this ->driver ->query ( 'SELECT 1 ' , PDO ::FETCH_ASSOC , 0 );
130+ }
131+
132+ public function test_query_fetch_default_mode_allow_any_args (): void {
133+ if ( PHP_VERSION_ID < 80100 ) {
134+ // On PHP < 8.1, fetch mode value of NULL is not allowed.
135+ $ result = @$ this ->driver ->query ( 'SELECT 1 ' , null , 1 , 2 , 'abc ' , array (), true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
136+ $ this ->assertFalse ( $ result );
137+ $ this ->assertSame ( 'PDO::query(): SQLSTATE[HY000]: General error: mode must be an integer ' , error_get_last ()['message ' ] );
138+ return ;
139+ }
140+
141+ // On PHP >= 8.1, NULL fetch mode is allowed to use the default fetch mode.
142+ // In such cases, any additional arguments are ignored and not validated.
143+ $ expected_result = array (
144+ array (
145+ 1 => '1 ' ,
146+ 0 => '1 ' ,
147+ ),
148+ );
149+
150+ $ result = $ this ->driver ->query ( 'SELECT 1 ' );
151+ $ this ->assertSame ( $ expected_result , $ result ->fetchAll () );
152+
153+ $ result = $ this ->driver ->query ( 'SELECT 1 ' , null );
154+ $ this ->assertSame ( $ expected_result , $ result ->fetchAll () );
155+
156+ $ result = $ this ->driver ->query ( 'SELECT 1 ' , null , 1 );
157+ $ this ->assertSame ( $ expected_result , $ result ->fetchAll () );
158+
159+ $ result = $ this ->driver ->query ( 'SELECT 1 ' , null , 'abc ' );
160+ $ this ->assertSame ( $ expected_result , $ result ->fetchAll () );
161+
162+ $ result = $ this ->driver ->query ( 'SELECT 1 ' , null , 1 , 2 , 'abc ' , array (), true );
163+ $ this ->assertSame ( $ expected_result , $ result ->fetchAll () );
164+ }
165+
166+ public function test_query_fetch_class_not_enough_args (): void {
167+ $ this ->expectException ( ArgumentCountError::class );
168+ $ this ->expectExceptionMessage ( 'PDO::query() expects at least 3 arguments for the fetch mode provided, 2 given ' );
169+ $ this ->driver ->query ( 'SELECT 1 ' , PDO ::FETCH_CLASS );
170+ }
171+
172+ public function test_query_fetch_class_too_many_args (): void {
173+ $ this ->expectException ( ArgumentCountError::class );
174+ $ this ->expectExceptionMessage ( 'PDO::query() expects at most 4 arguments for the fetch mode provided, 5 given ' );
175+ $ this ->driver ->query ( 'SELECT 1 ' , PDO ::FETCH_CLASS , '\stdClass ' , array (), array () );
176+ }
177+
178+ public function test_query_fetch_class_invalid_class_type (): void {
179+ $ this ->expectException ( TypeError::class );
180+ $ this ->expectExceptionMessage ( 'PDO::query(): Argument #3 must be of type string, int given ' );
181+ $ this ->driver ->query ( 'SELECT 1 ' , PDO ::FETCH_CLASS , 1 );
182+ }
183+
184+ public function test_query_fetch_class_invalid_class_name (): void {
185+ $ this ->expectException ( TypeError::class );
186+ $ this ->expectExceptionMessage ( 'PDO::query(): Argument #3 must be a valid class ' );
187+ $ this ->driver ->query ( 'SELECT 1 ' , PDO ::FETCH_CLASS , 'non-existent-class ' );
188+ }
189+
190+ public function test_query_fetch_class_invalid_constructor_args_type (): void {
191+ $ this ->expectException ( TypeError::class );
192+ $ this ->expectExceptionMessage ( 'PDO::query(): Argument #4 must be of type ?array, int given ' );
193+ $ this ->driver ->query ( 'SELECT 1 ' , PDO ::FETCH_CLASS , 'stdClass ' , 1 );
194+ }
195+
196+ public function test_query_fetch_into_invalid_arg_count (): void {
197+ $ this ->expectException ( ArgumentCountError::class );
198+ $ this ->expectExceptionMessage ( 'PDO::query() expects exactly 3 arguments for the fetch mode provided, 2 given ' );
199+ $ this ->driver ->query ( 'SELECT 1 ' , PDO ::FETCH_INTO );
200+ }
201+
202+ public function test_query_fetch_into_invalid_object_type (): void {
203+ $ this ->expectException ( TypeError::class );
204+ $ this ->expectExceptionMessage ( 'PDO::query(): Argument #3 must be of type object, int given ' );
205+ $ this ->driver ->query ( 'SELECT 1 ' , PDO ::FETCH_INTO , 1 );
206+ }
207+
208+ public function test_exec (): void {
209+ $ result = $ this ->driver ->exec ( 'SELECT 1 ' );
210+ $ this ->assertEquals ( 0 , $ result );
211+
212+ $ result = $ this ->driver ->exec ( 'CREATE TABLE t (id INT) ' );
213+ $ this ->assertEquals ( 0 , $ result );
214+
215+ $ result = $ this ->driver ->exec ( 'INSERT INTO t (id) VALUES (1) ' );
216+ $ this ->assertEquals ( 1 , $ result );
217+
218+ $ result = $ this ->driver ->exec ( 'INSERT INTO t (id) VALUES (2), (3) ' );
219+ $ this ->assertEquals ( 2 , $ result );
220+
221+ $ result = $ this ->driver ->exec ( 'UPDATE t SET id = 10 + id WHERE id = 0 ' );
222+ $ this ->assertEquals ( 0 , $ result );
223+
224+ $ result = $ this ->driver ->exec ( 'UPDATE t SET id = 10 + id WHERE id = 1 ' );
225+ $ this ->assertEquals ( 1 , $ result );
226+
227+ $ result = $ this ->driver ->exec ( 'UPDATE t SET id = 10 + id WHERE id < 10 ' );
228+ $ this ->assertEquals ( 2 , $ result );
229+
230+ $ result = $ this ->driver ->exec ( 'DELETE FROM t WHERE id = 11 ' );
231+ $ this ->assertEquals ( 1 , $ result );
232+
233+ $ result = $ this ->driver ->exec ( 'DELETE FROM t ' );
234+ $ this ->assertEquals ( 2 , $ result );
235+
236+ $ result = $ this ->driver ->exec ( 'DROP TABLE t ' );
237+ $ this ->assertEquals ( 0 , $ result );
12238 }
13239
14240 public function test_begin_transaction (): void {
@@ -50,4 +276,162 @@ public function test_rollback_no_active_transaction(): void {
50276 $ this ->expectExceptionCode ( 0 );
51277 $ this ->driver ->rollBack ();
52278 }
279+
280+ public function test_fetch_default (): void {
281+ // Default fetch mode is PDO::FETCH_BOTH.
282+ $ result = $ this ->driver ->query ( "SELECT 1, 'abc', 2 " );
283+ if ( PHP_VERSION_ID < 80000 ) {
284+ $ this ->assertSame (
285+ array (
286+ 1 => '1 ' ,
287+ 2 => '2 ' ,
288+ 'abc ' => 'abc ' ,
289+ 3 => 'abc ' ,
290+ 4 => '2 ' ,
291+ ),
292+ $ result ->fetch ()
293+ );
294+ } else {
295+ $ this ->assertSame (
296+ array (
297+ 1 => '1 ' ,
298+ 0 => '1 ' ,
299+ 'abc ' => 'abc ' ,
300+ '2 ' => '2 ' ,
301+ ),
302+ $ result ->fetch ()
303+ );
304+ }
305+ }
306+
307+ /**
308+ * @dataProvider data_pdo_fetch_methods
309+ */
310+ public function test_fetch ( $ query , $ mode , $ expected ): void {
311+ $ stmt = $ this ->driver ->query ( $ query );
312+ $ result = $ stmt ->fetch ( $ mode );
313+
314+ if ( is_object ( $ expected ) ) {
315+ $ this ->assertInstanceOf ( get_class ( $ expected ), $ result );
316+ $ this ->assertEquals ( $ expected , $ result );
317+ } elseif ( PDO ::FETCH_NAMED === $ mode ) {
318+ // PDO::FETCH_NAMED returns all array keys as strings, even numeric
319+ // ones. This is not possible in plain PHP and might be a PDO bug.
320+ $ this ->assertSame ( array_map ( 'strval ' , array_keys ( $ expected ) ), array_keys ( $ result ) );
321+ $ this ->assertSame ( array_values ( $ expected ), array_values ( $ result ) );
322+ } else {
323+ $ this ->assertSame ( $ expected , $ result );
324+ }
325+ }
326+
327+ public function test_attr_default_fetch_mode (): void {
328+ $ this ->driver ->setAttribute ( PDO ::ATTR_DEFAULT_FETCH_MODE , PDO ::FETCH_NUM );
329+ $ result = $ this ->driver ->query ( "SELECT 'a', 'b', 'c' " );
330+ $ this ->assertSame (
331+ array ( 'a ' , 'b ' , 'c ' ),
332+ $ result ->fetch ()
333+ );
334+
335+ $ this ->driver ->setAttribute ( PDO ::ATTR_DEFAULT_FETCH_MODE , PDO ::FETCH_ASSOC );
336+ $ result = $ this ->driver ->query ( "SELECT 'a', 'b', 'c' " );
337+ $ this ->assertSame (
338+ array (
339+ 'a ' => 'a ' ,
340+ 'b ' => 'b ' ,
341+ 'c ' => 'c ' ,
342+ ),
343+ $ result ->fetch ()
344+ );
345+ }
346+
347+ public function test_attr_stringify_fetches (): void {
348+ $ this ->driver ->setAttribute ( PDO ::ATTR_STRINGIFY_FETCHES , true );
349+ $ result = $ this ->driver ->query ( "SELECT 123, 1.23, 'abc', true, false " );
350+ $ this ->assertSame (
351+ array ( '123 ' , '1.23 ' , 'abc ' , '1 ' , '0 ' ),
352+ $ result ->fetch ( PDO ::FETCH_NUM )
353+ );
354+
355+ $ this ->driver ->setAttribute ( PDO ::ATTR_STRINGIFY_FETCHES , false );
356+ $ result = $ this ->driver ->query ( "SELECT 123, 1.23, 'abc', true, false " );
357+ $ this ->assertSame (
358+ /*
359+ * On PHP < 8.1, "PDO::ATTR_STRINGIFY_FETCHES" set to "false" has no
360+ * effect when "PDO::ATTR_EMULATE_PREPARES" is "true" (the default).
361+ *
362+ * TODO: Consider supporting non-string values on PHP < 8.1 when both
363+ * "PDO::ATTR_STRINGIFY_FETCHES" and "PDO::ATTR_EMULATE_PREPARES"
364+ * are set to "false". This would require emulating the behavior,
365+ * as PDO SQLite on PHP < 8.1 seems to always return strings.
366+ */
367+ PHP_VERSION_ID < 80100
368+ ? array ( '123 ' , '1.23 ' , 'abc ' , '1 ' , '0 ' )
369+ : array ( 123 , 1.23 , 'abc ' , 1 , 0 ),
370+ $ result ->fetch ( PDO ::FETCH_NUM )
371+ );
372+ }
373+
374+ public function data_pdo_fetch_methods (): Generator {
375+ // PDO::FETCH_BOTH
376+ yield 'PDO::FETCH_BOTH ' => array (
377+ "SELECT 1, 'abc', 2, 'two' as `2` " ,
378+ PDO ::FETCH_BOTH ,
379+ PHP_VERSION_ID < 80000
380+ ? array (
381+ 1 => '1 ' ,
382+ 2 => 'two ' ,
383+ 'abc ' => 'abc ' ,
384+ 3 => 'abc ' ,
385+ 4 => '2 ' ,
386+ 5 => 'two ' ,
387+ )
388+ : array (
389+ 1 => '1 ' ,
390+ 0 => '1 ' ,
391+ 'abc ' => 'abc ' ,
392+ 2 => 'two ' ,
393+ 3 => 'two ' ,
394+ ),
395+ );
396+
397+ // PDO::FETCH_NUM
398+ yield 'PDO::FETCH_NUM ' => array (
399+ "SELECT 1, 'abc', 2, 'two' as `2` " ,
400+ PDO ::FETCH_NUM ,
401+ array ( '1 ' , 'abc ' , '2 ' , 'two ' ),
402+ );
403+
404+ // PDO::FETCH_ASSOC
405+ yield 'PDO::FETCH_ASSOC ' => array (
406+ "SELECT 1, 'abc', 2, 'two' as `2` " ,
407+ PDO ::FETCH_ASSOC ,
408+ array (
409+ 1 => '1 ' ,
410+ 'abc ' => 'abc ' ,
411+ 2 => 'two ' ,
412+ ),
413+ );
414+
415+ // PDO::FETCH_NAMED
416+ yield 'PDO::FETCH_NAMED ' => array (
417+ "SELECT 1, 'abc', 2, 'two' as `2` " ,
418+ PDO ::FETCH_NAMED ,
419+ array (
420+ 1 => '1 ' ,
421+ 'abc ' => 'abc ' ,
422+ 2 => array ( '2 ' , 'two ' ),
423+ ),
424+ );
425+
426+ // PDO::FETCH_OBJ
427+ yield 'PDO::FETCH_OBJ ' => array (
428+ "SELECT 1, 'abc', 2, 'two' as `2` " ,
429+ PDO ::FETCH_OBJ ,
430+ (object ) array (
431+ 1 => '1 ' ,
432+ 'abc ' => 'abc ' ,
433+ 2 => 'two ' ,
434+ ),
435+ );
436+ }
53437}
0 commit comments