Skip to content
Open
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
17bcbf6
Introduce a method for inserting multiple rows into a table at once.
johnbillion Sep 1, 2023
986ec52
Introduce functions for bulk inserting metadata.
johnbillion Sep 1, 2023
26d8b49
Add PHPBench tests.
johnbillion Sep 1, 2023
7539a67
Fix indentation level.
johnbillion Sep 1, 2023
ccfd9f2
More test fixes.
johnbillion Sep 2, 2023
bde79b0
Disable these workflows while we're getting set up.
johnbillion Sep 2, 2023
63ad9a0
Docs.
johnbillion Sep 2, 2023
25568cf
More tests.
johnbillion Sep 2, 2023
6628c5d
Add all the other bulk meta functions.
johnbillion Sep 2, 2023
ae4d0e2
Some implementations. Not fully tested yet.
johnbillion Sep 2, 2023
bd44522
Docs.
johnbillion Sep 2, 2023
8b20883
Revert "Disable these workflows while we're getting set up."
johnbillion Sep 2, 2023
8636e97
Add some PHPBench config.
johnbillion Sep 2, 2023
fd188a7
Looks like we need a build here.
johnbillion Sep 2, 2023
41d4f0f
Merge branch 'trunk' into 59269
johnbillion May 26, 2025
0519ff5
Docs.
johnbillion May 26, 2025
3412032
Tweak this signature.
johnbillion May 26, 2025
fcf5696
Merge branch 'trunk' into 59269
johnbillion May 26, 2025
2bdfb62
Use the built-in field format mapping.
johnbillion May 28, 2025
ec59fbf
Workflow tweaks.
johnbillion May 28, 2025
3b17bd6
Give me strength.
johnbillion May 28, 2025
96d3494
Use Ubuntu 24.04.
johnbillion May 28, 2025
3561331
Docs.
johnbillion May 28, 2025
28f77ca
Refine the handling of mids and the metadata hooks.
johnbillion May 28, 2025
fcbae4e
More test updates.
johnbillion May 28, 2025
764c4fe
Docs.
johnbillion May 28, 2025
33bbd80
Reinstate slashed data handling.
johnbillion May 28, 2025
b01ea4a
Docs.
johnbillion May 28, 2025
0403bae
Formatting.
johnbillion May 28, 2025
c2cfd4a
Don't need this.
johnbillion May 28, 2025
60b07f9
Update the phpbench workflow.
johnbillion May 28, 2025
532267b
Coding standards.
johnbillion May 28, 2025
77a2dd3
Coding standards.
johnbillion May 28, 2025
e56a4de
More phpbench adjustments.
johnbillion May 28, 2025
bf3f6c8
Don't need to run the build here.
johnbillion May 28, 2025
9c47a61
Remove an unnecessary wpdb property.
johnbillion May 28, 2025
c2ae9aa
Add coverage annotations.
johnbillion May 28, 2025
71768ab
Rename these benchmark tests.
johnbillion May 28, 2025
fa73e3d
Let's get an actual PHPBench comparison working.
johnbillion May 28, 2025
26fea60
Tidying up.
johnbillion May 28, 2025
f4fc114
Apply suggestions from code review
johnbillion May 29, 2025
41d8473
More typos.
johnbillion May 29, 2025
e09e8b0
Apply suggestions from code review
johnbillion May 29, 2025
db01cb1
More integer casting.
johnbillion May 29, 2025
b8429c7
Make this more readable.
johnbillion May 29, 2025
18bde2a
Merge branch 'trunk' into 59269
johnbillion May 29, 2025
14f6187
Docs.
johnbillion Sep 10, 2025
03697e0
Use MockAction for filter call count assertions.
johnbillion Sep 10, 2025
7299c45
Add an assertion for the query count.
johnbillion Sep 10, 2025
3f3e1c4
Coding standards.
johnbillion Sep 10, 2025
cec8a66
Merge branch 'trunk' into 59269
johnbillion Sep 10, 2025
a142584
Save ourselves some trouble. This is a temporary workflow.
johnbillion Sep 10, 2025
7cfb673
Fix the images for 8.3 and 8.4.
johnbillion Sep 11, 2025
f84a39c
Correct this property.
johnbillion Sep 11, 2025
1d03913
Merge branch 'trunk' into 59269
johnbillion Oct 19, 2025
54f7d51
Document site meta. But it's blog meta. For a site.
johnbillion Oct 19, 2025
9a11cc5
Adjust the tests to cover all meta types.
johnbillion Oct 19, 2025
30dca5b
Remove PHPBench infrastructure.
johnbillion Oct 19, 2025
3a037e2
Docs.
johnbillion Oct 19, 2025
01428b9
One more.
johnbillion Oct 19, 2025
caf0bcc
Coding standards.
johnbillion Oct 19, 2025
1f197a2
Yoink.
johnbillion Oct 19, 2025
0b37c70
Why doesn't this get detected with `composer lint`?
johnbillion Oct 19, 2025
752cd68
Merge branch 'trunk' into 59269
johnbillion Dec 18, 2025
280454f
Update some fixture names.
johnbillion Dec 19, 2025
369d2e7
Apply suggestions from code review
johnbillion Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 106 additions & 1 deletion src/wp-includes/class-wpdb.php
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ class wpdb {
*
* @see wpdb::prepare()
* @see wpdb::insert()
* @see wpdb::insert_multiple()
* @see wpdb::update()
* @see wpdb::delete()
* @see wp_set_wpdb_vars()
Expand Down Expand Up @@ -2498,7 +2499,7 @@ public function remove_placeholder_escape( $query ) {
* Both `$data` columns and `$data` values should be "raw" (neither should be SQL escaped).
* Sending a null value will cause the column to be set to NULL - the corresponding
* format is ignored in this case.
* @param string[]|string $format Optional. An array of formats to be mapped to each of the value in `$data`.
* @param string[]|string $format Optional. An array of formats to be mapped to each of the values in `$data`.
* If string, that format will be used for all of the values in `$data`.
* A format is one of '%d', '%f', '%s' (integer, float, string).
* If omitted, all values in `$data` will be treated as strings unless otherwise
Expand All @@ -2509,6 +2510,110 @@ public function insert( $table, $data, $format = null ) {
return $this->_insert_replace_helper( $table, $data, $format, 'INSERT' );
}

/**
* Inserts multiple rows into the table in one query.
*
* If the insert fails, no rows will be inserted. It's not possible for some rows
* to be inserted and not others.
*
* Examples:
*
* $wpdb->insert_multiple(
* 'table',
* array(
* 'column1',
* 'column2',
* ),
* array(
* array(
* 'column 1 value 1',
* 'column 2 value 1',
* ),
* array(
* 'column 1 value 2',
* 'column 2 value 2',
* ),
* array(
* 'column 1 value 3',
* 'column 2 value 3',
* ),
* )
* );
* $wpdb->insert_multiple(
* 'table',
* array(
* 'column1',
* 'column2',
* ),
* array(
* array(
* 'column 1 value 1',
* 1,
* ),
* array(
* 'column 1 value 2',
* 2,
* ),
* array(
* 'column 1 value 3',
* 3,
* ),
* ),
* array(
* '%s',
* '%d',
* )
* );
*
* @since x.y.z
*
* @param string $table Table name.
* @param string[] $columns Array of column names.
* @param array $rows Array of rows of values to insert. Values should be "raw" (should not be SQL escaped).
* Sending a null value will cause the column to be set to NULL - the corresponding
* format is ignored in this case.
* @param string[] $format Optional. An array of formats to be mapped to each of the values in each row.
* A format is one of '%d', '%f', '%s' (integer, float, string).
* If omitted, all values in `$data` will be treated as strings unless otherwise
Comment thread
johnbillion marked this conversation as resolved.
Outdated
* specified in wpdb::$field_types. Default empty array.
* @return int|false The number of rows inserted, or false on error.
*/
public function insert_multiple( string $table, array $columns, array $rows, array $format = array() ) {
$this->insert_id = 0;

$values_sql = array();
$values = array(
$table,
);

foreach ( $rows as $row ) {
$data = $this->process_fields( $table, array_combine( $columns, $row ), $format );
Comment on lines +2578 to +2584
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function does not validate that the $columns and $rows arrays are not empty, or that all rows have the same number of elements as the $columns array. If a row has fewer or more elements than columns, the array_combine() call on line 2584 will return false, which is then checked on line 2585. However, if $rows or $columns is empty, the function will generate an invalid SQL query. These edge cases should be validated and return false early.

Suggested change
$values_sql = array();
$values = array(
$table,
);
foreach ( $rows as $row ) {
$data = $this->process_fields( $table, array_combine( $columns, $row ), $format );
// Validate that we have at least one column and one row to insert.
if ( empty( $columns ) || empty( $rows ) ) {
return false;
}
$values_sql = array();
$values = array(
$table,
);
foreach ( $rows as $row ) {
// Each row must be an array with the same number of elements as the columns.
if ( ! is_array( $row ) || count( $row ) !== count( $columns ) ) {
return false;
}
$combined = array_combine( $columns, $row );
if ( false === $combined ) {
return false;
}
$data = $this->process_fields( $table, $combined, $format );

Copilot uses AI. Check for mistakes.
if ( false === $data ) {
return false;
}

$formats = array();
foreach ( $data as $value ) {
if ( is_null( $value['value'] ) ) {
$formats[] = 'NULL';
continue;
}

$formats[] = $value['format'];
$values[] = $value['value'];
}

$values_sql[] = '(' . implode( ', ', $formats ) . ')';
}

$all_formats = implode( ', ', $values_sql );
$fields = implode( '`, `', $columns );
$sql = "INSERT INTO %i (`$fields`) VALUES $all_formats";

$this->check_current_query = false;
return $this->query( $this->prepare( $sql, $values ) );
}

/**
* Replaces a row in the table or inserts it if it does not exist, based on a PRIMARY KEY or a UNIQUE index.
*
Expand Down
32 changes: 29 additions & 3 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,34 @@ function add_comment_meta( $comment_id, $meta_key, $meta_value, $unique = false
return add_metadata( 'comment', $comment_id, $meta_key, $meta_value, $unique );
}

/**
* Adds multiple items of meta data to a comment.
*
* This function is more performant than calling `add_comment_meta()` multiple times because it queries the database
* only once and clears the meta cache only once.
*
* Examples:
*
* bulk_add_comment_meta(
* $comment->comment_ID,
* array(
* 'meta_key_1' => 'value_1',
* 'meta_key_2' => 'value_2',
* )
* );
*
* For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
*
* @since x.y.z
*
* @param int $comment_id Comment ID.
* @param array<string,mixed> $meta_fields Metadata values keyed by their meta key. Values must be serializable if non-scalar.
* @return array<string,int>|false Array of meta IDs keyed by their meta key on success, false on failure.
*/
function bulk_add_comment_meta( int $comment_id, array $meta_fields ) {
return bulk_add_metadata( 'comment', $comment_id, $meta_fields );
}

/**
* Removes metadata matching criteria from a comment.
*
Expand Down Expand Up @@ -2113,9 +2141,7 @@ function wp_insert_comment( $commentdata ) {

// If metadata is provided, store it.
if ( isset( $commentdata['comment_meta'] ) && is_array( $commentdata['comment_meta'] ) ) {
foreach ( $commentdata['comment_meta'] as $meta_key => $meta_value ) {
add_comment_meta( $comment->comment_ID, $meta_key, $meta_value, true );
}
bulk_add_comment_meta( $comment->comment_ID, $commentdata['comment_meta'] );
Comment thread
johnbillion marked this conversation as resolved.
}

/**
Expand Down
117 changes: 117 additions & 0 deletions src/wp-includes/meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,123 @@ function add_metadata( $meta_type, $object_id, $meta_key, $meta_value, $unique =
return $mid;
}

/**
* Adds multiple items of metadata for the specified object.
*
* This function is more performant than calling `add_metadata()` multiple times because it queries the database only
* once and clears the meta cache only once.
*
* This function will always insert all of the provided metadata even if matching keys already exist. This behaviour
* matches that of add_metadata() when its `$unique` parameter is set to false.
*
* If the insert fails, no metadata will be inserted. It's not possible for a subset of the rows to be inserted.
*
* Examples:
*
* bulk_add_metadata(
* 'post',
* $post_id,
* array(
* 'meta_key_1' => 'value_1',
* 'meta_key_2' => 'value_2',
Comment thread
johnbillion marked this conversation as resolved.
* )
* );
*
* For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
*
* @since x.y.z
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $meta_type Type of object metadata is for. Accepts 'post', 'comment', 'term', 'user',
* 'blog', or any other object type with an associated meta table.
* @param int $object_id ID of the object metadata is for.
* @param array<string,mixed> $meta_fields Metadata values keyed by their meta key. Values must be serializable if non-scalar.
* @return array<string,int>|false Array of meta IDs keyed by their meta key on success, false on failure.
*/
function bulk_add_metadata( string $meta_type, int $object_id, array $meta_fields ) {
global $wpdb;

if ( ! $meta_type || ! $meta_fields ) {
return false;
}

$object_id = absint( $object_id );
if ( ! $object_id ) {
return false;
}

$table = _get_meta_table( $meta_type );
if ( ! $table ) {
return false;
}

$meta_subtype = get_object_subtype( $meta_type, $object_id );
$column = sanitize_key( $meta_type . '_id' );
$data = array();
$return = array();
$added_keys = array();

foreach ( $meta_fields as $meta_key => $meta_value ) {
// expected_slashed ($meta_key)
Comment thread
johnbillion marked this conversation as resolved.
Outdated
$meta_key = wp_unslash( $meta_key );
$meta_value = wp_unslash( $meta_value );
$meta_value = sanitize_meta( $meta_key, $meta_value, $meta_type, $meta_subtype );

/** This filter is documented in wp-includes/meta.php */
$check = apply_filters( "add_{$meta_type}_metadata", null, $object_id, $meta_key, $meta_value, false );
if ( null !== $check ) {
$return[ $meta_key ] = $check;
continue;
}

/** This action is documented in wp-includes/meta.php */
do_action( "add_{$meta_type}_meta", $object_id, $meta_key, $meta_value );

$added_keys[] = $meta_key;

$data[] = array(
$object_id,
$meta_key,
maybe_serialize( $meta_value ),
);
}

// If there is no data to save, don't attempt to save it.
if ( ! $data ) {
return $return;
}

$inserted = $wpdb->insert_multiple(
Comment thread
johnbillion marked this conversation as resolved.
$table,
array(
$column,
'meta_key',
'meta_value',
),
$data
);

if ( ! $inserted ) {
return false;
}

$first_mid = (int) $wpdb->insert_id;
$inserted_mids = range( $first_mid, $first_mid + $inserted - 1 );
$keyed_mids = array_combine( $added_keys, $inserted_mids );
Comment on lines +246 to +276
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the filter short-circuits a meta key (line 238-240), the function continues to the next key without tracking it in $added_keys. However, if there are duplicate meta keys in $meta_fields, this could cause issues where $added_keys and the actual inserted keys get out of sync, leading to incorrect meta ID mapping in the returned array. Consider validating that all keys in $meta_fields are unique, or handle duplicate keys explicitly.

Copilot uses AI. Check for mistakes.
$all_mids = array_merge( $return, $keyed_mids );

wp_cache_delete( $object_id, $meta_type . '_meta' );

foreach ( $data as $datum ) {
list( $object_id, $meta_key, $meta_value ) = $datum;
/** This action is documented in wp-includes/meta.php */
do_action( "added_{$meta_type}_meta", $all_mids[ $meta_key ], $object_id, $meta_key, $meta_value );
Comment thread
johnbillion marked this conversation as resolved.
Outdated
}

return $all_mids;
}

/**
* Updates metadata for the specified object. If no value already exists for the specified object
* ID and metadata key, the metadata will be added.
Expand Down
28 changes: 28 additions & 0 deletions src/wp-includes/ms-site.php
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,34 @@ function add_site_meta( $site_id, $meta_key, $meta_value, $unique = false ) {
return add_metadata( 'blog', $site_id, $meta_key, $meta_value, $unique );
}

/**
* Adds multiple items of meta data to a site.
*
* This function is more performant than calling `add_site_meta()` multiple times because it queries the database
* only once and clears the meta cache only once.
*
* Examples:
*
* bulk_add_site_meta(
* $site_id,
* array(
* 'meta_key_1' => 'value_1',
* 'meta_key_2' => 'value_2',
* )
* );
*
* For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
*
* @since x.y.z
*
* @param int $site_id Site ID.
* @param array<string,mixed> $meta_fields Metadata values keyed by their meta key. Values must be serializable if non-scalar.
* @return array<string,int>|false Array of meta IDs keyed by their meta key on success, false on failure.
*/
function bulk_add_site_meta( int $site_id, array $meta_fields ) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Site meta is weird, might be best not to make it weirder.

It's intended not to accept duplicates and be interacted with via the site option functions, see https://core.trac.wordpress.org/ticket/61467

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the ticket link. This new function uses the same filters and the same cache key(s) as the existing add_site_meta() function.

Until https://core.trac.wordpress.org/ticket/61467 is resolved do you think it's best to not introduce a new bulk_add_site_meta() function?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until https://core.trac.wordpress.org/ticket/61467 is resolved do you think it's best to not introduce a new bulk_add_site_meta() function?

I'm inclined not to add the new function while the other ticket is worked out, just to avoid adding to the surface area.

return bulk_add_metadata( 'site', $site_id, $meta_fields );
Comment thread
johnbillion marked this conversation as resolved.
Outdated
}

/**
* Removes metadata matching criteria from a site.
*
Expand Down
36 changes: 34 additions & 2 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -2635,6 +2635,34 @@ function add_post_meta( $post_id, $meta_key, $meta_value, $unique = false ) {
return add_metadata( 'post', $post_id, $meta_key, $meta_value, $unique );
}

/**
* Adds multiple items of meta data to a post.
*
* This function is more performant than calling `add_post_meta()` multiple times because it queries the database
* only once and clears the meta cache only once.
*
* Examples:
*
* bulk_add_post_meta(
* $post->ID,
* array(
* 'meta_key_1' => 'value_1',
* 'meta_key_2' => 'value_2',
* )
* );
*
* For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
*
* @since x.y.z
*
* @param int $post_id Post ID.
* @param array<string,mixed> $meta_fields Metadata values keyed by their meta key. Values must be serializable if non-scalar.
* @return array<string,int>|false Array of meta IDs keyed by their meta key on success, false on failure.
*/
function bulk_add_post_meta( int $post_id, array $meta_fields ) {
return bulk_add_metadata( 'post', $post_id, $meta_fields );
}

/**
* Deletes a post meta field for the given post ID.
*
Expand Down Expand Up @@ -4993,8 +5021,12 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true )
}

if ( ! empty( $postarr['meta_input'] ) ) {
foreach ( $postarr['meta_input'] as $field => $value ) {
update_post_meta( $post_id, $field, $value );
if ( $update ) {
foreach ( $postarr['meta_input'] as $field => $value ) {
update_post_meta( $post_id, $field, $value );
}
} else {
bulk_add_post_meta( $post_id, $postarr['meta_input'] );
}
}

Expand Down
Loading
Loading