[COT] `query()` method: add support for field queries (#34533)
* Bring Yoda back. * Add method to OrdersTableQuery to obtain table info for a given order field * First pass at field_query * Add tests * Add changelog * Use backticks for table names and aliases * Improve validation in field_query * Add some more tests
This commit is contained in:
parent
f9bea12589
commit
c9c2bfbf92
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: enhancement
|
||||
|
||||
Add support for complex field queries for orders.
|
|
@ -0,0 +1,356 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Provides the implementation for `field_query` in {@see OrdersTableQuery} used to build
|
||||
* complex queries against order fields in the database.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class OrdersTableFieldQuery {
|
||||
|
||||
/**
|
||||
* List of valid SQL operators to use as field_query 'compare' values.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const VALID_COMPARISON_OPERATORS = array(
|
||||
'=',
|
||||
'!=',
|
||||
'LIKE',
|
||||
'NOT LIKE',
|
||||
'IN',
|
||||
'NOT IN',
|
||||
'EXISTS',
|
||||
'NOT EXISTS',
|
||||
'RLIKE',
|
||||
'REGEXP',
|
||||
'NOT REGEXP',
|
||||
'>',
|
||||
'>=',
|
||||
'<',
|
||||
'<=',
|
||||
'BETWEEN',
|
||||
'NOT BETWEEN',
|
||||
);
|
||||
|
||||
/**
|
||||
* The original query object.
|
||||
*
|
||||
* @var OrdersTableQuery
|
||||
*/
|
||||
private $query = null;
|
||||
|
||||
/**
|
||||
* Determines whether the field query should produce no results due to an invalid argument.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $force_no_results = false;
|
||||
|
||||
/**
|
||||
* Holds a sanitized version of the `field_query`.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $queries = array();
|
||||
|
||||
/**
|
||||
* JOIN clauses to add to the main SQL query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $join = array();
|
||||
|
||||
/**
|
||||
* WHERE clauses to add to the main SQL query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $where = array();
|
||||
|
||||
/**
|
||||
* Table aliases in use by the field query. Used to keep track of JOINs and optimize when possible.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $table_aliases = array();
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param OrdersTableQuery $q The main query being performed.
|
||||
*/
|
||||
public function __construct( OrdersTableQuery $q ) {
|
||||
$field_query = $q->get( 'field_query' );
|
||||
|
||||
if ( ! $field_query || ! is_array( $field_query ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->query = $q;
|
||||
$this->queries = $this->sanitize_query( $field_query );
|
||||
$this->where = ( ! $this->force_no_results ) ? $this->process( $this->queries ) : '1=0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the field_query argument.
|
||||
*
|
||||
* @param array $q A field_query array.
|
||||
* @return array A sanitized field query array.
|
||||
* @throws \Exception When field table info is missing.
|
||||
*/
|
||||
private function sanitize_query( array $q ) {
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $q as $key => $arg ) {
|
||||
if ( 'relation' === $key ) {
|
||||
$relation = $arg;
|
||||
} elseif ( ! is_array( $arg ) ) {
|
||||
continue;
|
||||
} elseif ( $this->is_atomic( $arg ) ) {
|
||||
if ( isset( $arg['value'] ) && array() === $arg['value'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sanitize 'compare'.
|
||||
$arg['compare'] = strtoupper( $arg['compare'] ?? '=' );
|
||||
$arg['compare'] = in_array( $arg['compare'], self::VALID_COMPARISON_OPERATORS, true ) ? $arg['compare'] : '=';
|
||||
|
||||
if ( '=' === $arg['compare'] && isset( $arg['value'] ) && is_array( $arg['value'] ) ) {
|
||||
$arg['compare'] = 'IN';
|
||||
}
|
||||
|
||||
// Sanitize 'cast'.
|
||||
$arg['cast'] = $this->sanitize_cast_type( $arg['type'] ?? '' );
|
||||
|
||||
$field_info = $this->query->get_field_mapping_info( $arg['field'] );
|
||||
if ( ! $field_info ) {
|
||||
$this->force_no_results = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$arg = array_merge( $arg, $field_info );
|
||||
|
||||
$sanitized[ $key ] = $arg;
|
||||
} else {
|
||||
$sanitized_arg = $this->sanitize_query( $arg );
|
||||
|
||||
if ( $sanitized_arg ) {
|
||||
$sanitized[ $key ] = $sanitized_arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $sanitized ) {
|
||||
$sanitized['relation'] = 1 === count( $sanitized ) ? 'OR' : $this->sanitize_relation( $relation ?? 'AND' );
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure we use an AND or OR relation. Defaults to AND.
|
||||
*
|
||||
* @param string $relation An unsanitized relation prop.
|
||||
* @return string
|
||||
*/
|
||||
private function sanitize_relation( string $relation ): string {
|
||||
if ( ! empty( $relation ) && 'OR' === strtoupper( $relation ) ) {
|
||||
return 'OR';
|
||||
}
|
||||
|
||||
return 'AND';
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes field_query entries and generates the necessary table aliases, JOIN statements and WHERE conditions.
|
||||
*
|
||||
* @param array $q A field query.
|
||||
* @return string An SQL WHERE statement.
|
||||
*/
|
||||
private function process( array $q ) {
|
||||
$where = '';
|
||||
|
||||
if ( empty( $q ) ) {
|
||||
return $where;
|
||||
}
|
||||
|
||||
if ( $this->is_atomic( $q ) ) {
|
||||
$q['alias'] = $this->find_or_create_table_alias_for_clause( $q );
|
||||
$where = $this->generate_where_for_clause( $q );
|
||||
} else {
|
||||
$relation = $q['relation'];
|
||||
unset( $q['relation'] );
|
||||
|
||||
foreach ( $q as $query ) {
|
||||
$chunks[] = $this->process( $query );
|
||||
}
|
||||
|
||||
if ( 1 === count( $chunks ) ) {
|
||||
$where = $chunks[0];
|
||||
} else {
|
||||
$where = '(' . implode( " {$relation} ", $chunks ) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given field_query clause is atomic or not (i.e. not nested).
|
||||
*
|
||||
* @param array $q The field_query clause.
|
||||
* @return boolean TRUE if atomic, FALSE otherwise.
|
||||
*/
|
||||
private function is_atomic( $q ) {
|
||||
return isset( $q['field'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a common table alias that the field_query clause can use, or creates one.
|
||||
*
|
||||
* @param array $q An atomic field_query clause.
|
||||
* @return string A table alias for use in an SQL JOIN clause.
|
||||
* @throws \Exception When table info for clause is missing.
|
||||
*/
|
||||
private function find_or_create_table_alias_for_clause( $q ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! empty( $q['alias'] ) ) {
|
||||
return $q['alias'];
|
||||
}
|
||||
|
||||
if ( empty( $q['table'] ) || empty( $q['column'] ) ) {
|
||||
throw new \Exception( __( 'Missing table info for query arg.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$join = '';
|
||||
|
||||
if ( isset( $q['mapping_id'] ) ) {
|
||||
// Re-use JOINs and aliases from OrdersTableQuery for core tables.
|
||||
$alias = $this->query->get_core_mapping_alias( $q['mapping_id'] );
|
||||
$join = $this->query->get_core_mapping_join( $q['mapping_id'] );
|
||||
} else {
|
||||
$alias = $q['table'];
|
||||
$join = '';
|
||||
}
|
||||
|
||||
if ( in_array( $alias, $this->table_aliases, true ) ) {
|
||||
return $alias;
|
||||
}
|
||||
|
||||
$this->table_aliases[] = $alias;
|
||||
|
||||
if ( $join ) {
|
||||
$this->join[] = $join;
|
||||
}
|
||||
|
||||
return $alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the correct type for a given clause 'type'.
|
||||
*
|
||||
* @param string $type MySQL type.
|
||||
* @return string MySQL type.
|
||||
*/
|
||||
private function sanitize_cast_type( $type ) {
|
||||
$clause_type = strtoupper( $type );
|
||||
|
||||
if ( ! $clause_type || ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $clause_type ) ) {
|
||||
return 'CHAR';
|
||||
}
|
||||
|
||||
if ( 'NUMERIC' === $clause_type ) {
|
||||
$clause_type = 'SIGNED';
|
||||
}
|
||||
|
||||
return $clause_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL WHERE clause for a given field_query atomic clause.
|
||||
*
|
||||
* @param array $clause An atomic field_query clause.
|
||||
* @return string An SQL WHERE clause or an empty string if $clause is invalid.
|
||||
*/
|
||||
private function generate_where_for_clause( $clause ): string {
|
||||
global $wpdb;
|
||||
|
||||
$clause_value = $clause['value'] ?? '';
|
||||
|
||||
if ( in_array( $clause['compare'], array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
|
||||
if ( ! is_array( $clause_value ) ) {
|
||||
$clause_value = preg_split( '/[,\s]+/', $clause_value );
|
||||
}
|
||||
} elseif ( is_string( $clause_value ) ) {
|
||||
$clause_value = trim( $clause_value );
|
||||
}
|
||||
|
||||
$clause_compare = $clause['compare'];
|
||||
|
||||
switch ( $clause_compare ) {
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $clause_value ) ), 1 ) . ')', $clause_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
case 'BETWEEN':
|
||||
case 'NOT BETWEEN':
|
||||
$where = $wpdb->prepare( '%s AND %s', $clause_value[0], $clause_value[1] ?? $clause_value[0] );
|
||||
break;
|
||||
case 'LIKE':
|
||||
case 'NOT LIKE':
|
||||
$where = $wpdb->prepare( '%s', '%' . $wpdb->esc_like( $clause_value ) . '%' );
|
||||
break;
|
||||
case 'EXISTS':
|
||||
// EXISTS with a value is interpreted as '='.
|
||||
if ( $clause_value ) {
|
||||
$clause_compare = '=';
|
||||
$where = $wpdb->prepare( '%s', $clause_value );
|
||||
} else {
|
||||
$clause_compare = 'IS NOT';
|
||||
$where = 'NULL';
|
||||
}
|
||||
|
||||
break;
|
||||
case 'NOT EXISTS':
|
||||
// 'value' is ignored for NOT EXISTS.
|
||||
$clause_compare = 'IS';
|
||||
$where = 'NULL';
|
||||
break;
|
||||
default:
|
||||
$where = $wpdb->prepare( '%s', $clause_value );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $where ) {
|
||||
if ( 'CHAR' === $clause['cast'] ) {
|
||||
return "`{$clause['alias']}`.`{$clause['column']}` {$clause_compare} {$where}";
|
||||
} else {
|
||||
return "CAST(`{$clause['alias']}`.`{$clause['column']}` AS {$clause['cast']}) {$clause_compare} {$where}";
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns JOIN and WHERE clauses to be appended to the main SQL query.
|
||||
*
|
||||
* @return array {
|
||||
* @type string $join JOIN clause.
|
||||
* @type string $where WHERE clause.
|
||||
* }
|
||||
*/
|
||||
public function get_sql_clauses() {
|
||||
return array(
|
||||
'join' => $this->join,
|
||||
'where' => $this->where ? array( $this->where ) : array(),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -129,6 +129,13 @@ class OrdersTableQuery {
|
|||
*/
|
||||
private $found_orders = 0;
|
||||
|
||||
/**
|
||||
* Field query parser.
|
||||
*
|
||||
* @var OrdersTableFieldQuery
|
||||
*/
|
||||
private $field_query = null;
|
||||
|
||||
/**
|
||||
* Meta query parser.
|
||||
*
|
||||
|
@ -527,6 +534,14 @@ class OrdersTableQuery {
|
|||
private function build_query(): void {
|
||||
$this->maybe_remap_args();
|
||||
|
||||
// Field queries.
|
||||
if ( ! empty( $this->args['field_query'] ) ) {
|
||||
$this->field_query = new OrdersTableFieldQuery( $this );
|
||||
$sql = $this->field_query->get_sql_clauses();
|
||||
$this->join = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join;
|
||||
$this->where = $sql['where'] ? array_merge( $this->where, $sql['where'] ) : $this->where;
|
||||
}
|
||||
|
||||
// Build query.
|
||||
$this->process_date_args();
|
||||
$this->process_orders_table_query_args();
|
||||
|
@ -578,7 +593,7 @@ class OrdersTableQuery {
|
|||
}
|
||||
|
||||
// JOIN.
|
||||
$join = implode( ' ', $this->join );
|
||||
$join = implode( ' ', array_unique( array_filter( array_map( 'trim', $this->join ) ) ) );
|
||||
|
||||
// WHERE.
|
||||
$where = '1=1';
|
||||
|
@ -594,7 +609,7 @@ class OrdersTableQuery {
|
|||
|
||||
if ( ! empty( $this->limits ) && count( $this->limits ) === 2 ) {
|
||||
list( $offset, $row_count ) = $this->limits;
|
||||
$row_count = $row_count === -1 ? self::MYSQL_MAX_UNSIGNED_BIGINT : (int) $row_count;
|
||||
$row_count = -1 === $row_count ? self::MYSQL_MAX_UNSIGNED_BIGINT : (int) $row_count;
|
||||
$limits = 'LIMIT ' . (int) $offset . ', ' . $row_count;
|
||||
}
|
||||
|
||||
|
@ -604,6 +619,55 @@ class OrdersTableQuery {
|
|||
$this->sql = "SELECT $found_rows DISTINCT $fields FROM $orders_table $join WHERE $where $groupby $orderby $limits";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table alias for a given table mapping.
|
||||
*
|
||||
* @param string $mapping_id The mapping name (e.g. 'orders' or 'operational_data').
|
||||
* @return string Table alias.
|
||||
*
|
||||
* @since 7.0.0
|
||||
*/
|
||||
public function get_core_mapping_alias( string $mapping_id ): string {
|
||||
return in_array( $mapping_id, array( 'billing_address', 'shipping_address' ), true )
|
||||
? $mapping_id
|
||||
: $this->tables[ $mapping_id ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an SQL JOIN clause that can be used to join the main orders table with another order table.
|
||||
*
|
||||
* @param string $mapping_id The mapping name (e.g. 'orders' or 'operational_data').
|
||||
* @return string The JOIN clause.
|
||||
*
|
||||
* @since 7.0.0
|
||||
*/
|
||||
public function get_core_mapping_join( string $mapping_id ): string {
|
||||
global $wpdb;
|
||||
|
||||
if ( 'orders' === $mapping_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$is_address_mapping = in_array( $mapping_id, array( 'billing_address', 'shipping_address' ), true );
|
||||
|
||||
$alias = $this->get_core_mapping_alias( $mapping_id );
|
||||
$table = $is_address_mapping ? $this->tables['addresses'] : $this->tables[ $mapping_id ];
|
||||
$join = '';
|
||||
$join_on = '';
|
||||
|
||||
$join .= "INNER JOIN `{$table}`" . ( $alias !== $table ? " AS `{$alias}`" : '' );
|
||||
|
||||
if ( isset( $this->mappings[ $mapping_id ]['order_id'] ) ) {
|
||||
$join_on .= "`{$this->tables['orders']}`.id = `{$alias}`.order_id";
|
||||
}
|
||||
|
||||
if ( $is_address_mapping ) {
|
||||
$join_on .= $wpdb->prepare( " AND `{$alias}`.address_type = %s", substr( $mapping_id, 0, -8 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
return $join . ( $join_on ? " ON ( {$join_on} )" : '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* JOINs the main orders table with another table.
|
||||
*
|
||||
|
@ -912,11 +976,11 @@ class OrdersTableQuery {
|
|||
$offset = ( $this->arg_isset( 'offset' ) ? absint( $this->args['offset'] ) : false );
|
||||
|
||||
// Bool false indicates no limit was specified; less than -1 means an invalid value was passed (such as -3).
|
||||
if ( $row_count === false || $row_count < -1 ) {
|
||||
if ( false === $row_count || $row_count < -1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $offset === false && $row_count > -1 ) {
|
||||
if ( false === $offset && $row_count > -1 ) {
|
||||
$offset = (int) ( ( $page - 1 ) * $row_count );
|
||||
}
|
||||
|
||||
|
@ -1006,4 +1070,73 @@ class OrdersTableQuery {
|
|||
return $this->tables[ $table_id ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds table and mapping information about a field or column.
|
||||
*
|
||||
* @param string $field Field to look for in `<mapping|field_name>.<column|field_name>` format or just `<field_name>`.
|
||||
* @return false|array {
|
||||
* @type string $table Full table name where the field is located.
|
||||
* @type string $mapping_id Unprefixed table or mapping name.
|
||||
* @type string $field_name Name of the corresponding order field.
|
||||
* @type string $column Column in $table that corresponds to the field.
|
||||
* @type string $type Field type.
|
||||
* }
|
||||
*/
|
||||
public function get_field_mapping_info( $field ) {
|
||||
global $wpdb;
|
||||
|
||||
$result = array(
|
||||
'table' => '',
|
||||
'mapping_id' => '',
|
||||
'field_name' => '',
|
||||
'column' => '',
|
||||
'column_type' => '',
|
||||
);
|
||||
|
||||
$mappings_to_search = array();
|
||||
|
||||
if ( false !== strstr( $field, '.' ) ) {
|
||||
list( $mapping_or_table, $field_name_or_col ) = explode( '.', $field );
|
||||
|
||||
$mapping_or_table = substr( $mapping_or_table, 0, strlen( $wpdb->prefix ) ) === $wpdb->prefix ? substr( $mapping_or_table, strlen( $wpdb->prefix ) ) : $mapping_or_table;
|
||||
$mapping_or_table = 'wc_' === substr( $mapping_or_table, 0, 3 ) ? substr( $mapping_or_table, 3 ) : $mapping_or_table;
|
||||
|
||||
if ( isset( $this->mappings[ $mapping_or_table ] ) ) {
|
||||
if ( isset( $this->mappings[ $mapping_or_table ][ $field_name_or_col ] ) ) {
|
||||
$result['mapping_id'] = $mapping_or_table;
|
||||
$result['column'] = $field_name_or_col;
|
||||
} else {
|
||||
$mappings_to_search = array( $mapping_or_table );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$field_name_or_col = $field;
|
||||
$mappings_to_search = array_keys( $this->mappings );
|
||||
}
|
||||
|
||||
foreach ( $mappings_to_search as $mapping_id ) {
|
||||
foreach ( $this->mappings[ $mapping_id ] as $column_name => $column_data ) {
|
||||
if ( isset( $column_data['name'] ) && $column_data['name'] === $field_name_or_col ) {
|
||||
$result['mapping_id'] = $mapping_id;
|
||||
$result['column'] = $column_name;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $result['mapping_id'] || ! $result['column'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$field_info = $this->mappings[ $result['mapping_id'] ][ $result['column'] ];
|
||||
|
||||
$result['field_name'] = $field_info['name'];
|
||||
$result['column_type'] = $field_info['type'];
|
||||
$result['table'] = ( in_array( $result['mapping_id'], array( 'billing_address', 'shipping_address' ), true ) )
|
||||
? $this->tables['addresses']
|
||||
: $this->tables[ $result['mapping_id'] ];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1087,6 +1087,220 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure field_query works as expected.
|
||||
*/
|
||||
public function test_cot_query_field_query(): void {
|
||||
$orders_test_data = array(
|
||||
array( 'Werner', 'Heisenberg', 'Unknown', 'werner_heisenberg_1', '15.0', '1901-12-05', '1976-02-01' ),
|
||||
array( 'Max', 'Planck', 'Quanta', 'planck_1', '16.0', '1858-04-23', '1947-10-04' ),
|
||||
array( 'Édouard', 'Roche', 'Tidal', 'roche_3', '9.99', '1820-10-17', '1883-04-27' ),
|
||||
);
|
||||
$order_ids = array();
|
||||
|
||||
// Create some test orders.
|
||||
foreach ( $orders_test_data as $i => $order_data ) {
|
||||
$order = new \WC_Order();
|
||||
$this->switch_data_store( $order, $this->sut );
|
||||
$order->set_status( 'wc-completed' );
|
||||
$order->set_shipping_city( 'The Universe' );
|
||||
$order->set_billing_first_name( $order_data[0] );
|
||||
$order->set_billing_last_name( $order_data[1] );
|
||||
$order->set_billing_city( $order_data[2] );
|
||||
$order->set_order_key( $order_data[3] );
|
||||
$order->set_total( $order_data[4] );
|
||||
|
||||
$order->add_meta_data( 'customer_birthdate', $order_data[5] );
|
||||
$order->add_meta_data( 'customer_last_seen', $order_data[6] );
|
||||
$order->add_meta_data( 'customer_age', absint( ( strtotime( $order_data[6] ) - strtotime( $order_data[5] ) ) / YEAR_IN_SECONDS ) );
|
||||
|
||||
$order_ids[] = $order->save();
|
||||
}
|
||||
|
||||
// Relatively simple field_query.
|
||||
$field_query = array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'field' => 'order_key',
|
||||
'value' => 'werner_heisenberg_1',
|
||||
),
|
||||
array(
|
||||
'field' => 'order_key',
|
||||
'value' => 'planck_1',
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertEqualsCanonicalizing( array( $order_ids[0], $order_ids[1] ), $query->orders );
|
||||
|
||||
// A more complex field_query.
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => 'billing_first_name',
|
||||
'value' => array( 'Werner', 'Max', 'Édouard' ),
|
||||
'compare' => 'IN',
|
||||
),
|
||||
array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'field' => 'billing_last_name',
|
||||
'value' => 'Heisen',
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
array(
|
||||
'field' => 'billing_city',
|
||||
'value' => 'Tid',
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
),
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertEqualsCanonicalizing( array( $order_ids[0], $order_ids[2] ), $query->orders );
|
||||
|
||||
// Find orders with order_key ending in a number (i.e. all).
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => 'order_key',
|
||||
'value' => '[0-9]$',
|
||||
'compare' => 'RLIKE'
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertEqualsCanonicalizing( $order_ids, $query->orders );
|
||||
|
||||
// Find orders with order_key not ending in a number (i.e. none).
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => 'order_key',
|
||||
'value' => '[^0-9]$',
|
||||
'compare' => 'NOT RLIKE'
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertCount( 0, $query->posts );
|
||||
|
||||
// Use full column name in a query.
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => $GLOBALS['wpdb']->prefix . 'wc_orders.total_amount',
|
||||
'value' => '10.0',
|
||||
'compare' => '<=',
|
||||
'type' => 'NUMERIC',
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertEqualsCanonicalizing( array( $order_ids[2] ), $query->orders );
|
||||
|
||||
// Pass an invalid column name.
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => 'non_existing_field',
|
||||
'value' => 'any-value',
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertCount( 0, $query->posts );
|
||||
|
||||
// Pass an apparently incorrect value to an 'IN' compare.
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => 'wc_orders.total_amount',
|
||||
'value' => 5.5,
|
||||
'compare' => 'IN',
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertCount( 0, $query->posts );
|
||||
|
||||
// Pass an invalid 'compare'.
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => 'wc_orders.total_amount',
|
||||
'value' => 10.0,
|
||||
'compare' => 'EXOSTS',
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertCount( 0, $query->posts );
|
||||
|
||||
// Pass an incomplete array for BETWEEN (treated as =).
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => 'total',
|
||||
'compare' => 'BETWEEN',
|
||||
'value' => 10.0,
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertCount( 0, $query->posts );
|
||||
|
||||
// Pass an incomplete array for NOT BETWEEN (treated as !=).
|
||||
$field_query = array(
|
||||
array(
|
||||
'field' => 'total',
|
||||
'compare' => 'NOT BETWEEN',
|
||||
'value' => array( 1.0 ),
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( array( 'field_query' => $field_query ) );
|
||||
$this->assertCount( 0, $query->posts );
|
||||
|
||||
// Test combinations of field_query with regular query args:
|
||||
$args = array(
|
||||
'id' => array( $order_ids[0], $order_ids[1] ),
|
||||
);
|
||||
$query = new OrdersTableQuery( $args );
|
||||
|
||||
// At this point 2 orders would be returned...
|
||||
$this->assertEqualsCanonicalizing( array( $order_ids[0], $order_ids[1] ), $query->orders );
|
||||
|
||||
// ... and now just one
|
||||
$args['field_query'] = array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'field' => 'id',
|
||||
'value' => $order_ids[1],
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( $args );
|
||||
$this->assertEqualsCanonicalizing( array( $order_ids[1] ), $query->orders );
|
||||
|
||||
// ... and now none (no orders below < 5.0)
|
||||
$args['field_query'][] = array(
|
||||
'field' => 'total',
|
||||
'value' => '5.0',
|
||||
'compare' => '<',
|
||||
);
|
||||
$query = new OrdersTableQuery( $args );
|
||||
$this->assertCount( 0, $query->orders );
|
||||
|
||||
// Now a more complex query with meta_query and date_query:
|
||||
$args = array(
|
||||
'shipping_address' => 'The Universe',
|
||||
'field_query' => array(
|
||||
array(
|
||||
'field' => 'total',
|
||||
'value' => array( 1.0, 11.0 ),
|
||||
'compare' => 'NOT BETWEEN',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// this should fetch the orders from Heisenberg and Planck...
|
||||
$query = new OrdersTableQuery( $args );
|
||||
$this->assertEqualsCanonicalizing( array( $order_ids[0], $order_ids[1] ), $query->orders );
|
||||
|
||||
// ... but only Planck is more than 80 years old.
|
||||
$args['meta_query'] = array(
|
||||
array(
|
||||
'key' => 'customer_age',
|
||||
'value' => 80,
|
||||
'compare' => '>='
|
||||
)
|
||||
);
|
||||
$query = new OrdersTableQuery( $args );
|
||||
$this->assertEqualsCanonicalizing( array( $order_ids[1] ), $query->orders );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that props set by datastores can be set and get by using any of metadata, object props or from data store setters.
|
||||
* Ideally, this should be possible only from getters and setters for objects, but for backward compatibility, earlier ways are also supported.
|
||||
|
@ -1267,4 +1481,5 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
|
|||
$this->assertEquals( $value, $order->{"get_$prop_name"}(), "Prop $prop_name was not set correctly." );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue