Skip to content

Commit 3523415

Browse files
committed
Implement and use PDOStatement fetch() and fetchAll() with basic fetch modes
1 parent d36091c commit 3523415

4 files changed

Lines changed: 248 additions & 21 deletions

File tree

tests/WP_PDO_MySQL_On_SQLite_PDO_API_Tests.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,87 @@ public function test_rollback_no_active_transaction(): void {
8686
$this->expectExceptionCode( 0 );
8787
$this->driver->rollBack();
8888
}
89+
90+
public function test_fetch_default(): void {
91+
// Default fetch mode is PDO::FETCH_BOTH.
92+
$result = $this->driver->query( "SELECT 1, 'abc', 2" );
93+
$this->assertSame(
94+
array(
95+
1 => 1,
96+
0 => 1,
97+
'abc' => 'abc',
98+
'2' => 2,
99+
),
100+
$result->fetch()
101+
);
102+
}
103+
104+
/**
105+
* @dataProvider data_pdo_fetch_methods
106+
*/
107+
public function test_fetch( $query, $mode, $expected ): void {
108+
$stmt = $this->driver->query( $query );
109+
$result = $stmt->fetch( $mode );
110+
if ( is_object( $expected ) ) {
111+
$this->assertInstanceOf( get_class( $expected ), $result );
112+
$this->assertEquals( $expected, $result );
113+
} else {
114+
$this->assertSame( $expected, $result );
115+
}
116+
}
117+
118+
public function data_pdo_fetch_methods(): Generator {
119+
// PDO::FETCH_BOTH
120+
yield 'PDO::FETCH_BOTH' => array(
121+
"SELECT 1, 'abc', 2, 'two' as `2`",
122+
PDO::FETCH_BOTH,
123+
array(
124+
1 => 1,
125+
0 => 1,
126+
'abc' => 'abc',
127+
'2' => 'two',
128+
'3' => 'two',
129+
),
130+
);
131+
132+
// PDO::FETCH_NUM
133+
yield 'PDO::FETCH_NUM' => array(
134+
"SELECT 1, 'abc', 2, 'two' as `2`",
135+
PDO::FETCH_NUM,
136+
array( 1, 'abc', 2, 'two' ),
137+
);
138+
139+
// PDO::FETCH_ASSOC
140+
yield 'PDO::FETCH_ASSOC' => array(
141+
"SELECT 1, 'abc', 2, 'two' as `2`",
142+
PDO::FETCH_ASSOC,
143+
array(
144+
'1' => 1,
145+
'abc' => 'abc',
146+
'2' => 'two',
147+
),
148+
);
149+
150+
// PDO::FETCH_NAMED
151+
yield 'PDO::FETCH_NAMED' => array(
152+
"SELECT 1, 'abc', 2, 'two' as `2`",
153+
PDO::FETCH_NAMED,
154+
array(
155+
'1' => 1,
156+
'abc' => 'abc',
157+
'2' => array( 2, 'two' ),
158+
),
159+
);
160+
161+
// PDO::FETCH_OBJ
162+
yield 'PDO::FETCH_OBJ' => array(
163+
"SELECT 1, 'abc', 2, 'two' as `2`",
164+
PDO::FETCH_OBJ,
165+
(object) array(
166+
'1' => 1,
167+
'abc' => 'abc',
168+
'2' => 'two',
169+
),
170+
);
171+
}
89172
}

wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ function ( string $sql, array $params ) {
727727
#[ReturnTypeWillChange]
728728
public function query( string $query, ?int $fetch_mode = PDO::FETCH_COLUMN, ...$fetch_mode_args ) {
729729
$this->flush();
730-
$this->pdo_fetch_mode = $fetch_mode;
730+
$this->pdo_fetch_mode = PDO::FETCH_NUM; // TODO
731731
$this->last_mysql_query = $query;
732732

733733
try {
@@ -772,8 +772,10 @@ public function query( string $query, ?int $fetch_mode = PDO::FETCH_COLUMN, ...$
772772
$this->commit_wrapper_transaction();
773773
}
774774

775+
$columns = is_array( $this->last_column_meta ) ? $this->last_column_meta : array();
776+
$rows = is_array( $this->last_result ) ? $this->last_result : array();
775777
$affected_rows = is_int( $this->last_return_value ) ? $this->last_return_value : 0;
776-
return new WP_PDO_Synthetic_Statement( $affected_rows );
778+
return new WP_PDO_Synthetic_Statement( $columns, $rows, $affected_rows );
777779
} catch ( Throwable $e ) {
778780
try {
779781
$this->rollback_user_transaction();
@@ -2444,7 +2446,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
24442446
} else {
24452447
$this->set_results_from_fetched_data(
24462448
array(
2447-
(object) array(
2449+
array(
24482450
'Table' => $table_name,
24492451
'Create Table' => $sql,
24502452
),
@@ -2483,7 +2485,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
24832485
case WP_MySQL_Lexer::GRANTS_SYMBOL:
24842486
$this->set_results_from_fetched_data(
24852487
array(
2486-
(object) array(
2488+
array(
24872489
'Grants for root@%' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION',
24882490
),
24892491
)
@@ -2572,7 +2574,7 @@ private function execute_show_collation_statement( WP_Parser_Node $node ): void
25722574
)
25732575
);
25742576
$this->store_last_column_meta_from_statement( $stmt );
2575-
$this->set_results_from_fetched_data( $stmt->fetchAll( PDO::FETCH_OBJ ) );
2577+
$this->set_results_from_fetched_data( $stmt->fetchAll( $this->pdo_fetch_mode ) );
25762578
}
25772579

25782580
/**
@@ -2604,7 +2606,7 @@ private function execute_show_databases_statement( WP_Parser_Node $node ): void
26042606
);
26052607

26062608
$this->store_last_column_meta_from_statement( $stmt );
2607-
$databases = $stmt->fetchAll( PDO::FETCH_OBJ );
2609+
$databases = $stmt->fetchAll( $this->pdo_fetch_mode );
26082610
$this->set_results_from_fetched_data( $databases );
26092611
}
26102612

@@ -2690,7 +2692,7 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void {
26902692
);
26912693

26922694
$this->store_last_column_meta_from_statement( $stmt );
2693-
$index_info = $stmt->fetchAll( PDO::FETCH_OBJ );
2695+
$index_info = $stmt->fetchAll( $this->pdo_fetch_mode );
26942696
$this->set_results_from_fetched_data( $index_info );
26952697
}
26962698

@@ -2753,7 +2755,7 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo
27532755
);
27542756

27552757
$this->store_last_column_meta_from_statement( $stmt );
2756-
$table_info = $stmt->fetchAll( PDO::FETCH_OBJ );
2758+
$table_info = $stmt->fetchAll( $this->pdo_fetch_mode );
27572759
if ( false === $table_info ) {
27582760
$this->set_results_from_fetched_data( array() );
27592761
}
@@ -2805,7 +2807,7 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void {
28052807
);
28062808

28072809
$this->store_last_column_meta_from_statement( $stmt );
2808-
$table_info = $stmt->fetchAll( PDO::FETCH_OBJ );
2810+
$table_info = $stmt->fetchAll( $this->pdo_fetch_mode );
28092811
if ( false === $table_info ) {
28102812
$this->set_results_from_fetched_data( array() );
28112813
}
@@ -2878,7 +2880,7 @@ private function execute_show_columns_statement( WP_Parser_Node $node ): void {
28782880
);
28792881

28802882
$this->store_last_column_meta_from_statement( $stmt );
2881-
$column_info = $stmt->fetchAll( PDO::FETCH_OBJ );
2883+
$column_info = $stmt->fetchAll( $this->pdo_fetch_mode );
28822884
if ( false === $column_info ) {
28832885
$this->set_results_from_fetched_data( array() );
28842886
}
@@ -2917,7 +2919,7 @@ private function execute_describe_statement( WP_Parser_Node $node ): void {
29172919
);
29182920

29192921
$this->store_last_column_meta_from_statement( $stmt );
2920-
$column_info = $stmt->fetchAll( PDO::FETCH_OBJ );
2922+
$column_info = $stmt->fetchAll( $this->pdo_fetch_mode );
29212923
$this->set_results_from_fetched_data( $column_info );
29222924
}
29232925

@@ -3239,14 +3241,14 @@ private function execute_administration_statement( WP_Parser_Node $node ): void
32393241

32403242
$operation = strtolower( $first_token->get_value() );
32413243
foreach ( $errors as $error ) {
3242-
$results[] = (object) array(
3244+
$results[] = array(
32433245
'Table' => $this->db_name . '.' . $table_name,
32443246
'Op' => $operation,
32453247
'Msg_type' => 'Error',
32463248
'Msg_text' => $error,
32473249
);
32483250
}
3249-
$results[] = (object) array(
3251+
$results[] = array(
32503252
'Table' => $this->db_name . '.' . $table_name,
32513253
'Op' => $operation,
32523254
'Msg_type' => 'status',

wp-includes/sqlite-ast/class-wp-pdo-synthetic-statement.php

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array {
7979
}
8080
}
8181

82+
/**
83+
* Polyfill ValueError for PHP < 8.0.
84+
*/
85+
if ( PHP_VERSION_ID < 80000 && ! class_exists( ValueError::class ) ) {
86+
class ValueError extends Error {
87+
}
88+
}
89+
8290
/**
8391
* PDOStatement implementation that operates on in-memory data.
8492
*
@@ -88,21 +96,64 @@ public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array {
8896
class WP_PDO_Synthetic_Statement extends PDOStatement {
8997
use WP_PDO_Synthetic_Statement_PHP_Compat;
9098

99+
/**
100+
* Basic column metadata (containing at least name, table name, and native type).
101+
*
102+
* @var array
103+
*/
104+
private $columns;
105+
106+
/**
107+
* Rows of the result set.
108+
*
109+
* @var array<array<mixed>>
110+
*/
111+
private $rows;
112+
91113
/**
92114
* The number of affected rows.
93115
*
94116
* @var int
95117
*/
96118
private $affected_rows;
97119

120+
/**
121+
* The current cursor offset.
122+
*
123+
* @var int
124+
*/
125+
private $cursor_offset = 0;
126+
127+
/**
128+
* The current fetch mode.
129+
*
130+
* TODO: Inherit this from "PDO::ATTR_DEFAULT_FETCH_MODE".
131+
*
132+
* @var int
133+
*/
134+
private $fetch_mode = PDO::FETCH_BOTH;
135+
136+
/**
137+
* Additional arguments for the current fetch mode.
138+
*
139+
* @var array<mixed>
140+
*/
141+
private $fetch_mode_args = array();
142+
98143
/**
99144
* Constructor.
100145
*
101-
* @param int $affected_rows The number of affected rows.
146+
* @param array $columns Basic column metadata (containing at least name, table name, and native type).
147+
* @param array $rows Rows of the result set.
148+
* @param int $affected_rows The number of affected rows.
102149
*/
103150
public function __construct(
104-
int $affected_rows = 0
151+
array $columns,
152+
array $rows,
153+
int $affected_rows
105154
) {
155+
$this->columns = $columns;
156+
$this->rows = $rows;
106157
$this->affected_rows = $affected_rows;
107158
}
108159

@@ -122,7 +173,7 @@ public function execute( $params = null ): bool {
122173
* @return int The number of columns in the result set.
123174
*/
124175
public function columnCount(): int {
125-
throw new RuntimeException( 'Not implemented' );
176+
return count( $this->columns );
126177
}
127178

128179
/**
@@ -152,7 +203,75 @@ public function fetch(
152203
$cursorOrientation = 0,
153204
$cursorOffset = 0
154205
) {
155-
throw new RuntimeException( 'Not implemented' );
206+
if ( 0 === $mode || null === $mode ) {
207+
$mode = $this->fetch_mode;
208+
}
209+
if ( null === $cursorOrientation ) {
210+
$cursorOrientation = PDO::FETCH_ORI_NEXT;
211+
}
212+
if ( null === $cursorOffset ) {
213+
$cursorOffset = 0;
214+
}
215+
216+
if ( ! array_key_exists( $this->cursor_offset, $this->rows ) ) {
217+
return false;
218+
}
219+
220+
// Get current row data and column names.
221+
$row = $this->rows[ $this->cursor_offset ];
222+
$column_names = array_column( $this->columns, 'name' );
223+
224+
// Advance the cursor to the next row.
225+
$this->cursor_offset += 1;
226+
227+
/*
228+
* TODO: Support scrollable cursor ($cursorOrientation and $cursorOffset).
229+
* This only has works for with statements that were prepared with
230+
* the PDO::ATTR_CURSOR attribute set to PDO::CURSOR_SCROLL value.
231+
* Without it, these parameters have no effect.
232+
*/
233+
234+
switch ( $mode ) {
235+
case PDO::FETCH_BOTH:
236+
$values = array();
237+
foreach ( $row as $i => $value ) {
238+
$name = $column_names[ $i ];
239+
$values[ $name ] = $value;
240+
if ( ! array_key_exists( $i, $values ) ) {
241+
$values[ $i ] = $value;
242+
}
243+
}
244+
return $values;
245+
case PDO::FETCH_NUM:
246+
return $row;
247+
case PDO::FETCH_ASSOC:
248+
return array_combine( $column_names, $row );
249+
case PDO::FETCH_NAMED:
250+
$values = array();
251+
foreach ( $row as $i => $value ) {
252+
$name = $column_names[ $i ];
253+
if ( is_array( $values[ $name ] ?? null ) ) {
254+
$values[ $name ][] = $value;
255+
} elseif ( array_key_exists( $name, $values ) ) {
256+
$values[ $name ] = array( $values[ $name ], $value );
257+
} else {
258+
$values[ $name ] = $value;
259+
}
260+
}
261+
return $values;
262+
case PDO::FETCH_OBJ:
263+
return (object) array_combine( $column_names, $row );
264+
case PDO::FETCH_CLASS:
265+
throw new RuntimeException( "'PDO::FETCH_CLASS' mode is not supported" );
266+
case PDO::FETCH_INTO:
267+
throw new RuntimeException( "'PDO::FETCH_INTO' mode is not supported" );
268+
case PDO::FETCH_LAZY:
269+
throw new RuntimeException( "'PDO::FETCH_LAZY' mode is not supported" );
270+
case PDO::FETCH_BOUND:
271+
throw new RuntimeException( "'PDO::FETCH_BOUND' mode is not supported" );
272+
default:
273+
throw new ValueError( sprintf( 'PDOStatement::fetch(): Argument #1 ($mode) must be a bitmask of PDO::FETCH_* constants', $mode ) );
274+
}
156275
}
157276

158277
/**
@@ -322,6 +441,14 @@ public function debugDumpParams(): ?bool {
322441
* @return array The result set as an array of rows.
323442
*/
324443
private function fetchAllRows( $mode = null, ...$args ): array {
325-
throw new RuntimeException( 'Not implemented' );
444+
if ( null === $mode || 0 === $mode ) {
445+
$mode = $this->fetch_mode;
446+
}
447+
448+
$rows = array();
449+
while ( $row = $this->fetch( $mode, ...$args ) ) {
450+
$rows[] = $row;
451+
}
452+
return $rows;
326453
}
327454
}

0 commit comments

Comments
 (0)