[HPOS] Add support for ordering by metadata in order queries (#36403)

This commit is contained in:
Barry Hughes 2023-01-13 10:20:49 -08:00 committed by GitHub
commit f84042a823
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 185 additions and 6 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add support for sorting by order metadata in HPOS queries.

View File

@ -73,6 +73,13 @@ class OrdersTableMetaQuery {
*/
private $queries = array();
/**
* Flat list of clauses by name.
*
* @var array
*/
private $flattened_clauses = array();
/**
* JOIN clauses to add to the main SQL query.
*
@ -129,6 +136,73 @@ class OrdersTableMetaQuery {
);
}
/**
* Returns a list of names (corresponding to meta_query clauses) that can be used as an 'orderby' arg.
*
* @since 7.4
*
* @return array
*/
public function get_orderby_keys(): array {
if ( ! $this->flattened_clauses ) {
return array();
}
$keys = array();
$keys[] = 'meta_value';
$keys[] = 'meta_value_num';
$first_clause = reset( $this->flattened_clauses );
if ( $first_clause && ! empty( $first_clause['key'] ) ) {
$keys[] = $first_clause['key'];
}
$keys = array_merge(
$keys,
array_keys( $this->flattened_clauses )
);
return $keys;
}
/**
* Returns an SQL fragment for the given meta_query key that can be used in an ORDER BY clause.
* Call {@see 'get_orderby_keys'} to obtain a list of valid keys.
*
* @since 7.4
*
* @param string $key The key name.
* @return string
*
* @throws \Exception When an invalid key is passed.
*/
public function get_orderby_clause_for_key( string $key ): string {
$clause = false;
if ( isset( $this->flattened_clauses[ $key ] ) ) {
$clause = $this->flattened_clauses[ $key ];
} else {
$first_clause = reset( $this->flattened_clauses );
if ( $first_clause && ! empty( $first_clause['key'] ) ) {
if ( 'meta_value_num' === $key ) {
return "{$first_clause['alias']}.meta_value+0";
}
if ( 'meta_value' === $key || $first_clause['key'] === $key ) {
$clause = $first_clause;
}
}
}
if ( ! $clause ) {
// translators: %s is a meta_query key.
throw new \Exception( sprintf( __( 'Invalid meta_query clause key: %s.', 'woocommerce' ), $key ) );
}
return "CAST({$clause['alias']}.meta_value AS {$clause['cast']})";
}
/**
* Checks whether a given meta_query clause is atomic or not (i.e. not nested).
*
@ -170,6 +244,7 @@ class OrdersTableMetaQuery {
}
$sanitized[ $key ] = $arg;
$sanitized[ $key ]['index'] = $key;
} else {
$sanitized_arg = $this->sanitize_meta_query( $arg );
@ -298,12 +373,24 @@ class OrdersTableMetaQuery {
$this->generate_where_for_clause_value( $arg ),
)
);
// Store clauses by their key for ORDER BY purposes.
$flat_clause_key = is_int( $arg['index'] ) ? $arg['alias'] : $arg['index'];
$unique_flat_key = $flat_clause_key;
$i = 1;
while ( isset( $this->flattened_clauses[ $unique_flat_key ] ) ) {
$unique_flat_key = $flat_clause_key . '-' . $i;
$i++;
}
$this->flattened_clauses[ $unique_flat_key ] =& $arg;
} else {
// Nested.
$relation = $arg['relation'];
unset( $arg['relation'] );
foreach ( $arg as $key => &$clause ) {
foreach ( $arg as $index => &$clause ) {
$chunks[] = $this->process( $clause, $arg );
}

View File

@ -522,11 +522,23 @@ class OrdersTableQuery {
}
}
$allowed_orderby = array_merge(
array_keys( $mapping ),
array_values( $mapping ),
$this->meta_query ? $this->meta_query->get_orderby_keys() : array()
);
$this->args['orderby'] = array();
foreach ( $orderby as $order_key => $order ) {
if ( isset( $mapping[ $order_key ] ) ) {
$this->args['orderby'][ $mapping[ $order_key ] ] = $this->sanitize_order( $order );
if ( ! in_array( $order_key, $allowed_orderby, true ) ) {
continue;
}
if ( isset( $mapping[ $order_key ] ) ) {
$order_key = $mapping[ $order_key ];
}
$this->args['orderby'][ $order_key ] = $this->sanitize_order( $order );
}
}
@ -991,8 +1003,14 @@ class OrdersTableQuery {
return;
}
$meta_orderby_keys = $this->meta_query ? $this->meta_query->get_orderby_keys() : array();
$orderby_array = array();
foreach ( $this->args['orderby'] as $_orderby => $order ) {
if ( in_array( $_orderby, $meta_orderby_keys, true ) ) {
$_orderby = $this->meta_query->get_orderby_clause_for_key( $_orderby );
}
$orderby_array[] = "{$_orderby} {$order}";
}

View File

@ -722,6 +722,76 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
// phpcs:enable
}
/**
* @testDox Tests queries involving 'orderby' and meta queries.
*/
public function test_cot_query_meta_orderby() {
$this->toggle_cot( true );
$order1 = new \WC_Order();
$order1->add_meta_data( 'color', 'red' );
$order1->add_meta_data( 'animal', 'lion' );
$order1->add_meta_data( 'numeric_meta', '1000' );
$order1->save();
$order2 = new \WC_Order();
$order2->add_meta_data( 'color', 'green' );
$order2->add_meta_data( 'animal', 'lion' );
$order2->add_meta_data( 'numeric_meta', '500' );
$order2->save();
$query_args = array(
'orderby' => 'id',
'order' => 'ASC',
'meta_key' => 'color', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
);
// Check that orders are in order (when no meta ordering is involved).
$q = new OrdersTableQuery( $query_args );
$this->assertEquals( $q->orders, array( $order1->get_id(), $order2->get_id() ) );
// When ordering by color $order2 should come first.
// Also tests that the key name is a valid synonym for the primary meta query.
$query_args['orderby'] = 'color';
$q = new OrdersTableQuery( $query_args );
$this->assertEquals( $q->orders, array( $order2->get_id(), $order1->get_id() ) );
// When ordering by 'numeric_meta' 1000 < 500 (due to alphabetical sorting by default).
// Also tests that 'meta_value' is a valid synonym for the primary meta query.
$query_args['meta_key'] = 'numeric_meta'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
$query_args['orderby'] = 'meta_value';
$q = new OrdersTableQuery( $query_args );
$this->assertEquals( $q->orders, array( $order1->get_id(), $order2->get_id() ) );
// Forcing numeric sorting with 'meta_value_num' reverses the order above.
$query_args['orderby'] = 'meta_value_num';
$q = new OrdersTableQuery( $query_args );
$this->assertEquals( $q->orders, array( $order2->get_id(), $order1->get_id() ) );
// Sorting by 'animal' meta is ambiguous. Test that we can order by various meta fields (and use the names in 'orderby').
unset( $query_args['meta_key'] );
$query_args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'animal_meta' => array(
'key' => 'animal',
),
'color_meta' => array(
'key' => 'color',
),
);
$query_args['orderby'] = array(
'animal_meta' => 'ASC',
'color_meta' => 'DESC',
);
$q = new OrdersTableQuery( $query_args );
$this->assertEquals( $q->orders, array( $order1->get_id(), $order2->get_id() ) );
// Order is reversed when changing the sort order for 'color_meta'.
$query_args['orderby']['color_meta'] = 'ASC';
$q = new OrdersTableQuery( $query_args );
$this->assertEquals( $q->orders, array( $order2->get_id(), $order1->get_id() ) );
}
/**
* @testDox Tests queries involving the 'customer' query var.
*