Skip to content

Commit 7f5e96d

Browse files
KentarouTakedadevnexen
authored andcommitted
ext/pdo_pgsql: Expanding COPY input from an array to an iterable
close GH-15893
1 parent 332e9a4 commit 7f5e96d

7 files changed

+177
-29
lines changed

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.5.0alpha1
44

5+
- PDO_PGSQL:
6+
. Added Iterable support for PDO::pgsqlCopyFromArray. (KentarouTakeda)
7+
58
<<< NOTE: Insert NEWS from last stable release here prior to actual release! >>>

UPGRADING

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ PHP 8.5 UPGRADE NOTES
3939
5. Changed Functions
4040
========================================
4141

42+
- PDO_PGSQL:
43+
. PDO::pgsqlCopyFromArray also supports inputs as Iterable.
44+
4245
========================================
4346
6. New Functions
4447
========================================

ext/pdo_pgsql/pgsql_driver.c

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "php_pdo_pgsql.h"
3232
#include "php_pdo_pgsql_int.h"
3333
#include "zend_exceptions.h"
34+
#include "zend_interfaces.h"
3435
#include "zend_smart_str.h"
3536
#include "pgsql_driver_arginfo.h"
3637

@@ -606,6 +607,32 @@ static bool pgsql_handle_rollback(pdo_dbh_t *dbh)
606607
return ret;
607608
}
608609

610+
static bool _pdo_pgsql_send_copy_data(pdo_pgsql_db_handle *H, zval *line) {
611+
size_t query_len;
612+
char *query;
613+
614+
if (!try_convert_to_string(line)) {
615+
return false;
616+
}
617+
618+
query_len = Z_STRLEN_P(line);
619+
query = emalloc(query_len + 2); /* room for \n\0 */
620+
memcpy(query, Z_STRVAL_P(line), query_len);
621+
622+
if (query[query_len - 1] != '\n') {
623+
query[query_len++] = '\n';
624+
}
625+
query[query_len] = '\0';
626+
627+
if (PQputCopyData(H->server, query, query_len) != 1) {
628+
efree(query);
629+
return false;
630+
}
631+
632+
efree(query);
633+
return true;
634+
}
635+
609636
void pgsqlCopyFromArray_internal(INTERNAL_FUNCTION_PARAMETERS)
610637
{
611638
pdo_dbh_t *dbh;
@@ -620,14 +647,14 @@ void pgsqlCopyFromArray_internal(INTERNAL_FUNCTION_PARAMETERS)
620647
PGresult *pgsql_result;
621648
ExecStatusType status;
622649

623-
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sa|sss!",
650+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sA|sss!",
624651
&table_name, &table_name_len, &pg_rows,
625652
&pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
626653
RETURN_THROWS();
627654
}
628655

629-
if (!zend_hash_num_elements(Z_ARRVAL_P(pg_rows))) {
630-
zend_argument_must_not_be_empty_error(2);
656+
if ((Z_TYPE_P(pg_rows) != IS_ARRAY && !instanceof_function(Z_OBJCE_P(pg_rows), zend_ce_traversable))) {
657+
zend_argument_type_error(2, "must be of type array or Traversable");
631658
RETURN_THROWS();
632659
}
633660

@@ -661,36 +688,35 @@ void pgsqlCopyFromArray_internal(INTERNAL_FUNCTION_PARAMETERS)
661688

662689
if (status == PGRES_COPY_IN && pgsql_result) {
663690
int command_failed = 0;
664-
size_t buffer_len = 0;
665691
zval *tmp;
692+
zend_object_iterator *iter;
666693

667694
PQclear(pgsql_result);
668-
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pg_rows), tmp) {
669-
size_t query_len;
670-
if (!try_convert_to_string(tmp)) {
671-
efree(query);
695+
696+
if (Z_TYPE_P(pg_rows) == IS_ARRAY) {
697+
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pg_rows), tmp) {
698+
if (!_pdo_pgsql_send_copy_data(H, tmp)) {
699+
pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
700+
PDO_HANDLE_DBH_ERR();
701+
RETURN_FALSE;
702+
}
703+
} ZEND_HASH_FOREACH_END();
704+
} else {
705+
iter = Z_OBJ_P(pg_rows)->ce->get_iterator(Z_OBJCE_P(pg_rows), pg_rows, 0);
706+
if (iter == NULL || EG(exception)) {
672707
RETURN_THROWS();
673708
}
674709

675-
if (buffer_len < Z_STRLEN_P(tmp)) {
676-
buffer_len = Z_STRLEN_P(tmp);
677-
query = erealloc(query, buffer_len + 2); /* room for \n\0 */
678-
}
679-
query_len = Z_STRLEN_P(tmp);
680-
memcpy(query, Z_STRVAL_P(tmp), query_len);
681-
if (query[query_len - 1] != '\n') {
682-
query[query_len++] = '\n';
683-
}
684-
query[query_len] = '\0';
685-
if (PQputCopyData(H->server, query, query_len) != 1) {
686-
efree(query);
687-
pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
688-
PDO_HANDLE_DBH_ERR();
689-
RETURN_FALSE;
710+
for (; iter->funcs->valid(iter) == SUCCESS && EG(exception) == NULL; iter->funcs->move_forward(iter)) {
711+
tmp = iter->funcs->get_current_data(iter);
712+
if (!_pdo_pgsql_send_copy_data(H, tmp)) {
713+
zend_iterator_dtor(iter);
714+
pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
715+
PDO_HANDLE_DBH_ERR();
716+
RETURN_FALSE;
717+
}
690718
}
691-
} ZEND_HASH_FOREACH_END();
692-
if (query) {
693-
efree(query);
719+
zend_iterator_dtor(iter);
694720
}
695721

696722
if (PQputCopyEnd(H->server, NULL) != 1) {

ext/pdo_pgsql/pgsql_driver.stub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99
class PDO_PGSql_Ext {
1010
/** @tentative-return-type */
11-
public function pgsqlCopyFromArray(string $tableName, array $rows, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
11+
public function pgsqlCopyFromArray(string $tableName, array | Traversable $rows, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
1212

1313
/** @tentative-return-type */
1414
public function pgsqlCopyFromFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}

ext/pdo_pgsql/pgsql_driver_arginfo.h

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
--TEST--
2+
PDO PgSQL pgsqlCopyFromArray using Generator
3+
--EXTENSIONS--
4+
pdo_pgsql
5+
--SKIPIF--
6+
<?php
7+
require __DIR__ . '/config.inc';
8+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
9+
PDOTest::skip();
10+
?>
11+
--FILE--
12+
<?php
13+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
14+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
15+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
16+
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
17+
18+
$db->exec('CREATE TABLE test_copy_from_generator (v int)');
19+
20+
$generator = (function(){
21+
$position = 0;
22+
$values = [1, 1, 2, 3, 5];
23+
24+
while(isset($values[$position])){
25+
yield $values[$position];
26+
++$position;
27+
}
28+
})();
29+
30+
31+
$db->pgsqlCopyFromArray('test_copy_from_generator',$generator);
32+
33+
$stmt = $db->query("select * from test_copy_from_generator order by 1");
34+
$result = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
35+
var_export($result);
36+
37+
?>
38+
--CLEAN--
39+
<?php
40+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
41+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
42+
$db->query('DROP TABLE IF EXISTS test_copy_from_generator CASCADE');
43+
?>
44+
--EXPECT--
45+
array (
46+
0 => 1,
47+
1 => 1,
48+
2 => 2,
49+
3 => 3,
50+
4 => 5,
51+
)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
--TEST--
2+
PDO PgSQL pgsqlCopyFromArray using Iterator
3+
--EXTENSIONS--
4+
pdo_pgsql
5+
--SKIPIF--
6+
<?php
7+
require __DIR__ . '/config.inc';
8+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
9+
PDOTest::skip();
10+
?>
11+
--FILE--
12+
<?php
13+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
14+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
15+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
16+
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
17+
18+
$db->exec('CREATE TABLE test_copy_from_traversable (v int)');
19+
20+
$iterator = new class implements Iterator{
21+
private $position = 0;
22+
private $values = [1, 1, 2, 3, 5];
23+
24+
public function rewind(): void {
25+
$this->position = 0;
26+
}
27+
28+
public function current(): int {
29+
return $this->values[$this->position];
30+
}
31+
32+
public function key(): int {
33+
return $this->position;
34+
}
35+
36+
public function next(): void {
37+
++$this->position;
38+
}
39+
40+
public function valid(): bool {
41+
return isset($this->values[$this->position]);
42+
}
43+
};
44+
45+
$db->pgsqlCopyFromArray('test_copy_from_traversable',$iterator);
46+
47+
$stmt = $db->query("select * from test_copy_from_traversable order by 1");
48+
$result = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
49+
var_export($result);
50+
51+
?>
52+
--CLEAN--
53+
<?php
54+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
55+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
56+
$db->query('DROP TABLE IF EXISTS test_copy_from_traversable CASCADE');
57+
?>
58+
--EXPECT--
59+
array (
60+
0 => 1,
61+
1 => 1,
62+
2 => 2,
63+
3 => 3,
64+
4 => 5,
65+
)

0 commit comments

Comments
 (0)