From 1eef6df713e5cb1c387868ab5fac4aac6929a716 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 29 Sep 2024 21:28:40 +0100 Subject: [PATCH 1/2] ext/pgsql: pgsql_copy_from to support iterable. inspired from the Pdo\Pgsql new feature GH-15893. --- ext/pgsql/pgsql.c | 76 +++++++++++++++------- ext/pgsql/pgsql.stub.php | 2 +- ext/pgsql/pgsql_arginfo.h | 4 +- ext/pgsql/tests/pg_copy_from_iterable.phpt | 62 ++++++++++++++++++ 4 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 ext/pgsql/tests/pg_copy_from_iterable.phpt diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 09ed9d75623cc..8222726ed62f3 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -38,6 +38,7 @@ #include "php_globals.h" #include "zend_exceptions.h" #include "zend_attributes.h" +#include "zend_interfaces.h" #include "php_network.h" #ifdef HAVE_PGSQL @@ -3357,6 +3358,29 @@ PHP_FUNCTION(pg_copy_to) } /* }}} */ +static zend_result pgsql_copy_from_query(PGconn *pgsql, PGresult *pgsql_result, zval *value) +{ + zend_string *tmp = zval_try_get_string(value); + if (UNEXPECTED(!tmp)) { + return FAILURE; + } + zend_string *zquery = zend_string_alloc(ZSTR_LEN(tmp) + 1, false); + memcpy(ZSTR_VAL(zquery), ZSTR_VAL(tmp), ZSTR_LEN(tmp) + 1); + ZSTR_LEN(zquery) = ZSTR_LEN(tmp); + if (ZSTR_LEN(tmp) > 0 && ZSTR_VAL(zquery)[ZSTR_LEN(tmp)] != '\n') { + ZSTR_VAL(zquery)[ZSTR_LEN(tmp)] = '\n'; + ZSTR_LEN(zquery) ++; + } + if (PQputCopyData(pgsql, ZSTR_VAL(zquery), ZSTR_LEN(zquery)) != 1) { + zend_string_release_ex(zquery, false); + zend_string_release(tmp); + return FAILURE; + } + zend_string_release_ex(zquery, false); + zend_string_release(tmp); + return SUCCESS; +} + /* {{{ Copy table from array */ PHP_FUNCTION(pg_copy_from) { @@ -3376,7 +3400,7 @@ PHP_FUNCTION(pg_copy_from) ZEND_PARSE_PARAMETERS_START(3, 5) Z_PARAM_OBJECT_OF_CLASS(pgsql_link, pgsql_link_ce) Z_PARAM_PATH_STR(table_name) - Z_PARAM_ARRAY(pg_rows) + Z_PARAM_ARRAY_OR_OBJECT(pg_rows) Z_PARAM_OPTIONAL Z_PARAM_STR(pg_delimiter) Z_PARAM_STRING(pg_null_as, pg_null_as_len) @@ -3392,6 +3416,10 @@ PHP_FUNCTION(pg_copy_from) zend_argument_value_error(4, "must be one character"); RETURN_THROWS(); } + if (Z_TYPE_P(pg_rows) == IS_OBJECT && !instanceof_function(Z_OBJCE_P(pg_rows), zend_ce_traversable)) { + zend_argument_type_error(3, "must be of type Traversable"); + RETURN_THROWS(); + } if (!pg_null_as) { pg_null_as = estrdup("\\\\N"); pg_null_as_free = true; @@ -3417,30 +3445,32 @@ PHP_FUNCTION(pg_copy_from) switch (status) { case PGRES_COPY_IN: if (pgsql_result) { - int command_failed = 0; PQclear(pgsql_result); - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pg_rows), value) { - zend_string *tmp = zval_try_get_string(value); - if (UNEXPECTED(!tmp)) { - return; - } - zend_string *zquery = zend_string_alloc(ZSTR_LEN(tmp) + 1, false); - memcpy(ZSTR_VAL(zquery), ZSTR_VAL(tmp), ZSTR_LEN(tmp) + 1); - ZSTR_LEN(zquery) = ZSTR_LEN(tmp); - if (ZSTR_LEN(tmp) > 0 && ZSTR_VAL(zquery)[ZSTR_LEN(tmp)] != '\n') { - ZSTR_VAL(zquery)[ZSTR_LEN(tmp)] = '\n'; - ZSTR_LEN(zquery) ++; - } - if (PQputCopyData(pgsql, ZSTR_VAL(zquery), ZSTR_LEN(zquery)) != 1) { - zend_string_release_ex(zquery, false); - zend_string_release(tmp); - PHP_PQ_ERROR("copy failed: %s", pgsql); - RETURN_FALSE; + bool command_failed = false; + if (Z_TYPE_P(pg_rows) == IS_ARRAY) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pg_rows), value) { + if (pgsql_copy_from_query(pgsql, pgsql_result, value) == FAILURE) { + PHP_PQ_ERROR("copy failed: %s", pgsql); + RETURN_FALSE; + } + } ZEND_HASH_FOREACH_END(); + } else { + zend_object_iterator *iter = Z_OBJ_P(pg_rows)->ce->get_iterator(Z_OBJCE_P(pg_rows), pg_rows, 0); + if (UNEXPECTED(EG(exception) || iter == NULL)) { + RETURN_THROWS(); } - zend_string_release_ex(zquery, false); - zend_string_release(tmp); - } ZEND_HASH_FOREACH_END(); + while (iter->funcs->valid(iter) == SUCCESS && EG(exception) == NULL) { + zval *value = iter->funcs->get_current_data(iter); + if (pgsql_copy_from_query(pgsql, pgsql_result, value) == FAILURE) { + zend_iterator_dtor(iter); + PHP_PQ_ERROR("copy failed: %s", pgsql); + RETURN_FALSE; + } + iter->funcs->move_forward(iter); + } + zend_iterator_dtor(iter); + } if (PQputCopyEnd(pgsql, NULL) != 1) { PHP_PQ_ERROR("putcopyend failed: %s", pgsql); RETURN_FALSE; @@ -3448,7 +3478,7 @@ PHP_FUNCTION(pg_copy_from) while ((pgsql_result = PQgetResult(pgsql))) { if (PGRES_COMMAND_OK != PQresultStatus(pgsql_result)) { PHP_PQ_ERROR("Copy command failed: %s", pgsql); - command_failed = 1; + command_failed = true; } PQclear(pgsql_result); } diff --git a/ext/pgsql/pgsql.stub.php b/ext/pgsql/pgsql.stub.php index 10ce60df9cdc8..1b362656eb459 100644 --- a/ext/pgsql/pgsql.stub.php +++ b/ext/pgsql/pgsql.stub.php @@ -847,7 +847,7 @@ function pg_put_line($connection, string $query = UNKNOWN): bool {} */ function pg_copy_to(PgSql\Connection $connection, string $table_name, string $separator = "\t", string $null_as = "\\\\N"): array|false {} - function pg_copy_from(PgSql\Connection $connection, string $table_name, array $rows, string $separator = "\t", string $null_as = "\\\\N"): bool {} + function pg_copy_from(PgSql\Connection $connection, string $table_name, array|object $rows, string $separator = "\t", string $null_as = "\\\\N"): bool {} /** * @param PgSql\Connection|string $connection diff --git a/ext/pgsql/pgsql_arginfo.h b/ext/pgsql/pgsql_arginfo.h index 778698a287563..5c8c581d99280 100644 --- a/ext/pgsql/pgsql_arginfo.h +++ b/ext/pgsql/pgsql_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 1f0141abe7cf476c305b074e31ce69a48b6eee21 */ + * Stub hash: 74009a62d922296c3f7a5d59b40591594acf1bbc */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_pg_connect, 0, 1, PgSql\\Connection, MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, connection_string, IS_STRING, 0) @@ -318,7 +318,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pg_copy_from, 0, 3, _IS_BOOL, 0) ZEND_ARG_OBJ_INFO(0, connection, PgSql\\Connection, 0) ZEND_ARG_TYPE_INFO(0, table_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, rows, IS_ARRAY, 0) + ZEND_ARG_TYPE_MASK(0, rows, MAY_BE_ARRAY|MAY_BE_OBJECT, NULL) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, separator, IS_STRING, 0, "\"\\t\"") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, null_as, IS_STRING, 0, "\"\\\\\\\\N\"") ZEND_END_ARG_INFO() diff --git a/ext/pgsql/tests/pg_copy_from_iterable.phpt b/ext/pgsql/tests/pg_copy_from_iterable.phpt new file mode 100644 index 0000000000000..f68ad89cf913d --- /dev/null +++ b/ext/pgsql/tests/pg_copy_from_iterable.phpt @@ -0,0 +1,62 @@ +--TEST-- +pg_copy_from with an iterable +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- +count; + } + + public function rewind(): void { + $this->count = 0; + } + + public function current(): int { + return $this->values[$this->count]; + } + + public function key(): int { + return $this->count; + } + + public function valid(): bool { + return $this->count < count($this->values); + } +}; + +try { + pg_copy_from($db, $table_name, new stdClass()); +} catch (\TypeError $e) { + echo $e->getMessage() . PHP_EOL; +} +var_dump(pg_copy_from($db, $table_name, $iter)); +$res = pg_query($db, "SELECT FROM {$table_name}"); +var_dump(count(pg_fetch_all($res)) == 3); + +?> +--CLEAN-- + +--EXPECT-- +pg_copy_from(): Argument #3 ($rows) must be of type Traversable +bool(true) +bool(true) From 495021f94b16572428785f96e1122a8c7adb62be Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 1 Oct 2024 18:39:09 +0100 Subject: [PATCH 2/2] changes from feedback --- ext/pgsql/pgsql.c | 12 ++++++------ ext/pgsql/pgsql.stub.php | 2 +- ext/pgsql/pgsql_arginfo.h | 4 ++-- ext/pgsql/tests/pg_copy_from_iterable.phpt | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 8222726ed62f3..f3e1a925dd5fc 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -3400,7 +3400,7 @@ PHP_FUNCTION(pg_copy_from) ZEND_PARSE_PARAMETERS_START(3, 5) Z_PARAM_OBJECT_OF_CLASS(pgsql_link, pgsql_link_ce) Z_PARAM_PATH_STR(table_name) - Z_PARAM_ARRAY_OR_OBJECT(pg_rows) + Z_PARAM_ITERABLE(pg_rows) Z_PARAM_OPTIONAL Z_PARAM_STR(pg_delimiter) Z_PARAM_STRING(pg_null_as, pg_null_as_len) @@ -3416,10 +3416,6 @@ PHP_FUNCTION(pg_copy_from) zend_argument_value_error(4, "must be one character"); RETURN_THROWS(); } - if (Z_TYPE_P(pg_rows) == IS_OBJECT && !instanceof_function(Z_OBJCE_P(pg_rows), zend_ce_traversable)) { - zend_argument_type_error(3, "must be of type Traversable"); - RETURN_THROWS(); - } if (!pg_null_as) { pg_null_as = estrdup("\\\\N"); pg_null_as_free = true; @@ -3455,11 +3451,15 @@ PHP_FUNCTION(pg_copy_from) } } ZEND_HASH_FOREACH_END(); } else { - zend_object_iterator *iter = Z_OBJ_P(pg_rows)->ce->get_iterator(Z_OBJCE_P(pg_rows), pg_rows, 0); + zend_object_iterator *iter = Z_OBJCE_P(pg_rows)->get_iterator(Z_OBJCE_P(pg_rows), pg_rows, 0); if (UNEXPECTED(EG(exception) || iter == NULL)) { RETURN_THROWS(); } + if (iter->funcs->rewind) { + iter->funcs->rewind(iter); + } + while (iter->funcs->valid(iter) == SUCCESS && EG(exception) == NULL) { zval *value = iter->funcs->get_current_data(iter); if (pgsql_copy_from_query(pgsql, pgsql_result, value) == FAILURE) { diff --git a/ext/pgsql/pgsql.stub.php b/ext/pgsql/pgsql.stub.php index 1b362656eb459..5da341b20ad52 100644 --- a/ext/pgsql/pgsql.stub.php +++ b/ext/pgsql/pgsql.stub.php @@ -847,7 +847,7 @@ function pg_put_line($connection, string $query = UNKNOWN): bool {} */ function pg_copy_to(PgSql\Connection $connection, string $table_name, string $separator = "\t", string $null_as = "\\\\N"): array|false {} - function pg_copy_from(PgSql\Connection $connection, string $table_name, array|object $rows, string $separator = "\t", string $null_as = "\\\\N"): bool {} + function pg_copy_from(PgSql\Connection $connection, string $table_name, array|Traversable $rows, string $separator = "\t", string $null_as = "\\\\N"): bool {} /** * @param PgSql\Connection|string $connection diff --git a/ext/pgsql/pgsql_arginfo.h b/ext/pgsql/pgsql_arginfo.h index 5c8c581d99280..52012afee070e 100644 --- a/ext/pgsql/pgsql_arginfo.h +++ b/ext/pgsql/pgsql_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 74009a62d922296c3f7a5d59b40591594acf1bbc */ + * Stub hash: 824e5aa07fd6753b5bc7821a39ccb76768f2470b */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_pg_connect, 0, 1, PgSql\\Connection, MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, connection_string, IS_STRING, 0) @@ -318,7 +318,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pg_copy_from, 0, 3, _IS_BOOL, 0) ZEND_ARG_OBJ_INFO(0, connection, PgSql\\Connection, 0) ZEND_ARG_TYPE_INFO(0, table_name, IS_STRING, 0) - ZEND_ARG_TYPE_MASK(0, rows, MAY_BE_ARRAY|MAY_BE_OBJECT, NULL) + ZEND_ARG_OBJ_TYPE_MASK(0, rows, Traversable, MAY_BE_ARRAY, NULL) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, separator, IS_STRING, 0, "\"\\t\"") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, null_as, IS_STRING, 0, "\"\\\\\\\\N\"") ZEND_END_ARG_INFO() diff --git a/ext/pgsql/tests/pg_copy_from_iterable.phpt b/ext/pgsql/tests/pg_copy_from_iterable.phpt index f68ad89cf913d..aadde2549a811 100644 --- a/ext/pgsql/tests/pg_copy_from_iterable.phpt +++ b/ext/pgsql/tests/pg_copy_from_iterable.phpt @@ -57,6 +57,6 @@ $db = pg_connect($conn_str); pg_query($db, "DROP TABLE IF EXISTS {$table_name}"); ?> --EXPECT-- -pg_copy_from(): Argument #3 ($rows) must be of type Traversable +pg_copy_from(): Argument #3 ($rows) must be of type Traversable|array, stdClass given bool(true) bool(true)