Hi Kamil,
> Regarding the progress indicator: it looks to me like it will be very
> difficult to implement properly in PHP, but I might be completely
> misunderstanding the design. If you think a PoC is possible, could you
> please prepare one and maybe then we can come back to this discussion.
>
> ----------------------------
>
We have callback function support in several extension - the closest
matching one is probably curl:
curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, 'progressCallback');
Even if no progress callback function was specified, we will always read
the progress information from server to avoid possible read timeouts for
long running operations.
> This is exactly what exceptions and errors are for. The PHP user
> SHOULD NOT be concerned with what capabilities the server offers and
> which are compatible with PHP. Even if it could be potentially useful
> to PHP developers, we should not expose this information as part of
> mysqli API. Either mysqli supports the feature or it doesn't; there
> should be no maybe.
>
> ----------------------------
>
I disagree — relying on try/catch is a waste of resources, especially in
high-load environments, when the information is already available locally.
It forces the application to parse error messages or perform unnecessary
network round-trips just to 'discover' server limitations.
More importantly, this approach is dangerous for data integrity. For
example, if you attempt to insert 1,000 rows via execute_many() on a MyISAM
table and the last row contains a feature (like an indicator variable) that
the server does not support, 999 rows will be committed before the error is
triggered. You cannot 'catch' your way out of a partially written batch in
a non-transactional engine.
Capability checking is a standard and necessary pattern in PHP. ext/gd uses
a bitmask for supported formats so developers can choose the correct logic
path before processing begins. Furthermore, ext/mysqli already exposes
several capability flags. While some were historically used for internal
purposes or inherited from libmysql (like SSL_VERIFY_SERVER_CERT), the
precedent for exposing capabilities to the user is already firmly
established in the current API.
>
>
> If mysqlnd is going to serialize it all into a large internal buffer
> then it still needs to read all the data from the iterator before
> making the query? It's still going to use the same amount of memory.
> How is it going to maintain constant memory usage?
>
Exactly. Because mysqlnd sits on top of PHP streams — which do not expose
direct access to their internal network buffers (at least not at the time
when Andrey wrote mysqlnd) —mysqlnd must allocate its own memory buffer for
each command before transferring it via the Stream API.
The memory usage is determined by the size of the protocol packet being
sent. To maintain efficiency, mysqlnd manages this buffer dynamically: it
reallocates the buffer only when necessary using an exponential growth
strategy to minimize overhead.
Regardless of whether the input is an array or a stream, the buffer must be
large enough to hold the serialized command. The 'constant memory' aspect
refers to the fact that we aren't duplicating the entire dataset multiple
times in different formats; rather, we are streaming the serialized data
into a managed internal buffer that mysqlnd already relies on for network
communication.
>
> A generator might be a neat trick for users to prepare data on the go,
> but it doesn't reduce the memory usage if the consumer needs to use
> all of the data in one go. As I understand, execute_many will send it
> all in one batch, so the memory footprint stays the same.
>
> ----------------------------
>
No, the peak memory usage is significantly lower. Consider the difference
between file() and fread():
- file()/Array: You must store the entire dataset in PHP memory as zvals,
then duplicate it into the mysqlnd network buffer. You are essentially
double-buffering.
- Generator/Stream: PHP memory remains constant because it only holds one
row at a time. mysqlnd pulls that row, serializes it directly into the
network buffer, and moves on.
By using a stream, you eliminate the massive overhead of the intermediate
PHP array, even if the final network packet requires a large buffer.
>
> Re execute_many parameters:
>
> Despite your convincing arguments for better network utilization by
> providing the types, I still think we should not offer the possibility
> of specifying the types. I don't know what other PHP developers on
> this mailing list think about it, but for me the type feature goes
> against the nature of PHP. Making the parameter optional is very good
> choice and eases my concerns slightly, so if I am outnumbered in my
> opinion, I won't be upset.
>
> The number of mysqli users grows increasingly smaller. Out of this,
> the number of people who will use execute_many and who will need to
> optimize for TINYINT is unbelievably tiny. Any string easily
> overshadows the numerical data. Thus, this feature won't see much
> legitimate use.
>
I agree that 99% of users likely won't specify types. However, there will
always be cases—such as limited memory or restricted CPU—where this
optional parameter is essential. It reduces the footprint and eliminates
the overhead of type conversions on both the client and the server.
Even if the primary use case is 'tiny,' a low-level driver like mysqlnd
should provide the tools for maximum efficiency, especially when the
implementation cost for the engine is minimal but the potential performance
gain for the user is high.
> > Because the MariaDB bulk protocol requires type declarations in the
> packet header before the data rows are sent, the driver cannot "autodetect"
> binary widths from a Generator without reading the entire stream into
> memory first. Providing the $types string acts as a contract that allows
> for true, constant-memory streaming.
>
> Isn't that what it does anyway? You need to read all the data
> (serialize) before you make the EXECUTE command, correct? I don't
> understand why you can't prepare the type specification automatically
> while serializing the data.
>
That works for arrays, but not for streams or generators. Unlike an array,
a stream is a one-way 'pull' mechanism—we don't know the type of the second
or hundredth row until we have already consumed the last.
To determine types automatically, we would have to buffer the entire stream
into memory first to inspect it, which completely defeats the purpose of
using a stream to save memory. Providing the types upfront allows mysqlnd
to serialize the stream directly to the wire in a single pass.
>
> Regarding the control parameter:
>
> Why not make it a callback? Provide the $row as an argument and let
> the user modify it inside the callback, substituting values for
> mysqli_indicator, and returning the row to be inserted. It would offer
> a lot more flexibility to the user and would make the implementation
> simpler. This way, you don't need to implement Null or None anymore.
>
> ----------------------------
>
A callback would actually be a major step backward for performance.
Invoking a PHP user-land closure 100,000 times in a single execute_many()
call introduces a massive overhead due to context switching between the
C-engine and the PHP VM. This would effectively negate the performance
gains we are trying to achieve.
My planned implementation already solves the flexibility problem via
Generators. Since execute_many() accepts an iterable, a user can already
use a generator to perform row-by-row logic. This is far more efficient
than a callback.
Furthermore, a callback does not eliminate the need for
mysqli_indicator::None. Even inside a function, the driver still needs a
clear 'metadata signal' to know whether to pull a value from the data
source or to treat it as an override.
I was asking you to list in the RFC all the possible client errors
> that are added as part of this implementation. For example, "Row %lu
> is not an array". This should be part of the RFC, in my opinion, as we
> may want to discuss the error conditions and messages too.
>
> ----------------------------
>
I haven't introduced any new error codes; all data validation errors use
CR_INVALID_PARAMETER_NO with UNKNOWN_SQLSTATE. I didn't see a requirement
in the RFC guidelines to list every specific error message, but if it is
mandatory for the process, I can add them to the document of course.
>
> Result sets:
>
> That's not what I meant. I was asking whether it could be implemented
> with MARIADB_CLIENT_BULK_UNIT_RESULTS instead. When users execute a
> SELECT with 2 data rows, I would like to see it return 2 mysqli_result
> objects. Same with INSERT statements, it should return a separate
> result for each insertion. If there are arguments against that, they
> should be explained in the RFC.
>
Expecting 1,000 separate result objects for 1,000 inserted rows would cause
a massive performance collapse. Each result set would require its own
network packet and redundant metadata headers, completely defeating the
purpose of a bulk execution API.
Regarding MARIADB_CLIENT_BULK_UNIT_RESULTS: this is a very recnt feature
(introduced in MariaDB 11.5) that allows the server to return a single
result package containing multiple status rows. I did not include it in the
current RFC because it is not yet widely available in LTS releases.
Furthermore, for the MySQL fallback (where this capability is absent),
mysqlnd would have to 'artificially' construct these result sets in memory,
which adds significant overhead. The goal of execute_many is maximum
throughput, which is best achieved by providing a summary of the bulk
operation rather than individual results for every row.
>
> Anyway, the RFC should clearly explain how result fetching works with
> all 3 methods (unbuffered, stored, and get_result) and what are the
> possible gotchas.
>
> ----------------------------
>
The RFC notes that execute_many() can return a result set (e.g., when using
a RETURNING clause or similar). The PHP documentation already clearly
defines how result sets are retrieved from prepared statements: via
bind_result() with store_result()/use_result(), or via get_result().
Since execute_many() follows the existing mysqli_stmt behavior and does not
change how results are buffered or fetched, adding a redundant explanation
of standard mysqli mechanisms would only clutter the RFC.
>
> > For transactional engines like InnoDB, atomicity is guaranteed. If a
> protocol error occurs or a constraint is hit mid-batch, the server handles
> the rollback, ensuring the database remains in a consistent state.
>
> > Transaction Safety & Atomicity: In native MariaDB bulk mode, the entire
> batch is sent as a single unit. In the fallback emulation, rows are
> executed one by one. For non-transactional engines, a failure on row 500
> would leave the first 499 rows committed. To maintain consistency, we
> should recommend that users wrap execute_many() in an explicit transaction
> when portability across MySQL and MariaDB is required.
>
> I am confused. Aren't both of these statements stating the same? Why
> can't you wrap the fallback in an automatic transaction to make it
> work exactly the same as the native MariaDB solution?
>
> If execute_many implies an automatic transaction but only with
> transactional engines, it should be clearly stated in the RFC so that
> it can be later documented in PHP manual too.
>
> ----------------------------
>
> I am against wrapping the fallback in an automatic transaction because a
low-level driver should not modify the session's transactional state behind
the scenes.
If mysqli were to automatically inject START TRANSACTION and COMMIT, it
could unexpectedly commit a user's existing work or interfere with their
manual transaction logic. Furthermore, an 'automatic' transaction would be
a false promise on engines like MyISAM, where BEGIN and COMMIT are simply
ignored, still resulting in partial inserts.
I've updated the RFC to clarify this.
> > Limited Indicator Support: Since the standard MySQL COM_STMT_EXECUTE
> protocol does not understand MariaDB-specific indicators, the fallback will
> only support mysqli_indicator::Null (translated to a standard SQL NULL) and
> mysqli_indicator::None. Indicators like DEFAULT and IGNORE are technically
> impossible to implement in the fallback without complex SQL string
> manipulation/rewriting, which would introduce unacceptable CPU overhead.
>
> And for this reason, I think that maybe we shouldn't even implement
> the control parameter at all. It sounds like a neat feature, but it
> costs performance in a function that is all about improving
> performance, and it is DB-version specific. The new execute_many
> function doesn't need the control parameter to function properly, and
> in my opinion, it would be better to keep it as simple as possible.
> But I am curious to see what others think.
>
Could you please explain why you expect a performance loss? I believe the
opposite is the case.
Consider the following common scenario: A system collects records from
various external APIs. The 'IDs' from these sources are not unique, so we
need the database to generate its own AUTO_INCREMENT keys. Additionally,
for privacy compliance (GDPR/CCPA), we must mask phone numbers during the
import.
The 'expensive' way would be to modify the existing data source. In the
worst case—for instance, if you need to log the original data after the
import—you would have to create a full copy of the data first. You then
have to iterate over the entire dataset to:
- Set every id to null to trigger AUTO_INCREMENT.
- Overwrite every phone number with a masked string (e.g., +XX XXX-XXXXXXX).
By specifying a control parameter, the source data remains completely
untouched. The driver handles both the nullification and the masking at the
C-level.
Example:
/* Raw external data: [External_ID, Name, Phone] */
$external_data = [
[101, 'John Doe', '555-1234'],
[102, 'Jane Doe', '555-5678'],
...
[100000, "Rasmus Lerdorf', '431-1233939']
];
/* Control parameter:
- Column 0 (ID): Force NULL to trigger AUTO_INCREMENT
- Column 1 (Name): Use mysqli_indicator::None (Keep original data)
- Column 2 (Phone): Scalar override for privacy masking */
$control = [
mysqli_indicator::Null,
mysqli_indicator::None,
"+XX XXX-XXXXXXX"
];
$stmt->execute_many($external_data, control: $control);
/Georg
--
Georg Richter, Staff Software Engineer
Client Connectivity
MariaDB Corporation Ab