[HPOS] Add support for ordering by metadata in order queries (#36403)
This commit is contained in:
commit
f84042a823
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add support for sorting by order metadata in HPOS queries.
|
|
@ -73,6 +73,13 @@ class OrdersTableMetaQuery {
|
||||||
*/
|
*/
|
||||||
private $queries = array();
|
private $queries = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flat list of clauses by name.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $flattened_clauses = array();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JOIN clauses to add to the main SQL query.
|
* 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).
|
* Checks whether a given meta_query clause is atomic or not (i.e. not nested).
|
||||||
*
|
*
|
||||||
|
@ -158,8 +232,8 @@ class OrdersTableMetaQuery {
|
||||||
unset( $arg['value'] );
|
unset( $arg['value'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
$arg['compare'] = isset( $arg['compare'] ) ? strtoupper( $arg['compare'] ) : ( isset( $arg['value'] ) && is_array( $arg['value'] ) ? 'IN' : '=' );
|
$arg['compare'] = isset( $arg['compare'] ) ? strtoupper( $arg['compare'] ) : ( isset( $arg['value'] ) && is_array( $arg['value'] ) ? 'IN' : '=' );
|
||||||
$arg['compare_key'] = isset( $arg['compare_key'] ) ? strtoupper( $arg['compare_key'] ) : ( isset( $arg['key'] ) && is_array( $arg['key'] ) ? 'IN' : '=' );
|
$arg['compare_key'] = isset( $arg['compare_key'] ) ? strtoupper( $arg['compare_key'] ) : ( isset( $arg['key'] ) && is_array( $arg['key'] ) ? 'IN' : '=' );
|
||||||
|
|
||||||
if ( ! in_array( $arg['compare'], self::NON_NUMERIC_OPERATORS, true ) && ! in_array( $arg['compare'], self::NUMERIC_OPERATORS, true ) ) {
|
if ( ! in_array( $arg['compare'], self::NON_NUMERIC_OPERATORS, true ) && ! in_array( $arg['compare'], self::NUMERIC_OPERATORS, true ) ) {
|
||||||
$arg['compare'] = '=';
|
$arg['compare'] = '=';
|
||||||
|
@ -169,7 +243,8 @@ class OrdersTableMetaQuery {
|
||||||
$arg['compare_key'] = '=';
|
$arg['compare_key'] = '=';
|
||||||
}
|
}
|
||||||
|
|
||||||
$sanitized[ $key ] = $arg;
|
$sanitized[ $key ] = $arg;
|
||||||
|
$sanitized[ $key ]['index'] = $key;
|
||||||
} else {
|
} else {
|
||||||
$sanitized_arg = $this->sanitize_meta_query( $arg );
|
$sanitized_arg = $this->sanitize_meta_query( $arg );
|
||||||
|
|
||||||
|
@ -298,12 +373,24 @@ class OrdersTableMetaQuery {
|
||||||
$this->generate_where_for_clause_value( $arg ),
|
$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 {
|
} else {
|
||||||
// Nested.
|
// Nested.
|
||||||
$relation = $arg['relation'];
|
$relation = $arg['relation'];
|
||||||
unset( $arg['relation'] );
|
unset( $arg['relation'] );
|
||||||
|
|
||||||
foreach ( $arg as $key => &$clause ) {
|
foreach ( $arg as $index => &$clause ) {
|
||||||
$chunks[] = $this->process( $clause, $arg );
|
$chunks[] = $this->process( $clause, $arg );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
$this->args['orderby'] = array();
|
||||||
foreach ( $orderby as $order_key => $order ) {
|
foreach ( $orderby as $order_key => $order ) {
|
||||||
if ( isset( $mapping[ $order_key ] ) ) {
|
if ( ! in_array( $order_key, $allowed_orderby, true ) ) {
|
||||||
$this->args['orderby'][ $mapping[ $order_key ] ] = $this->sanitize_order( $order );
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$meta_orderby_keys = $this->meta_query ? $this->meta_query->get_orderby_keys() : array();
|
||||||
|
|
||||||
$orderby_array = array();
|
$orderby_array = array();
|
||||||
foreach ( $this->args['orderby'] as $_orderby => $order ) {
|
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}";
|
$orderby_array[] = "{$_orderby} {$order}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -722,6 +722,76 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
|
||||||
// phpcs:enable
|
// 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.
|
* @testDox Tests queries involving the 'customer' query var.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue