protected function get_fields( $query_args ) {
if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) {
return $query_args['fields'];
return array_keys( $this->report_columns );
* Returns a comma separated list of the field names prepared to be used for a selection after a join with `default_results`.
* @param array $fields Array of fields name.
* @param array $default_results_fields Fields to load from `default_results` table.
* @param array $outer_selections Array of fields that are not selected in the inner query.
protected function format_join_selections( $fields, $default_results_fields, $outer_selections = array() ) {
foreach ( $fields as $i => $field ) {
foreach ( $default_results_fields as $default_results_field ) {
if ( $field === $default_results_field ) {
$field = esc_sql( $field );
$fields[ $i ] = "default_results.{$field} AS {$field}";
if ( in_array( $field, $outer_selections, true ) && array_key_exists( $field, $this->report_columns ) ) {
$fields[ $i ] = $this->report_columns[ $field ];
return implode( ', ', $fields );
* Fills ORDER BY clause of SQL request based on user supplied parameters.
* @param array $query_args Parameters supplied by the user.
protected function add_order_by_sql_params( $query_args ) {
if ( isset( $query_args['orderby'] ) ) {
$order_by_clause = $this->normalize_order_by( esc_sql( $query_args['orderby'] ) );
$this->clear_sql_clause( 'order_by' );
$this->add_sql_clause( 'order_by', $order_by_clause );
$this->add_orderby_order_clause( $query_args, $this );
* Fills FROM and WHERE clauses of SQL request for 'Intervals' section of data response based on user supplied parameters.
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint.
protected function add_intervals_sql_params( $query_args, $table_name ) {
$this->clear_sql_clause( array( 'from', 'where_time', 'where' ) );
$this->add_time_period_sql_params( $query_args, $table_name );
if ( isset( $query_args['interval'] ) && '' !== $query_args['interval'] ) {
$interval = $query_args['interval'];
$this->clear_sql_clause( 'select' );
$this->add_sql_clause( 'select', TimeInterval::db_datetime_format( $interval, $table_name, $this->date_column_name ) );
* Get join and where clauses for refunds based on user supplied parameters.
* @param array $query_args Parameters supplied by the user.
protected function get_refund_subquery( $query_args ) {
$table_name = $wpdb->prefix . 'wc_order_stats';
if ( ! isset( $query_args['refunds'] ) ) {
if ( 'all' === $query_args['refunds'] ) {
$sql_query['where_clause'] .= 'parent_id != 0';
if ( 'none' === $query_args['refunds'] ) {
$sql_query['where_clause'] .= 'parent_id = 0';
if ( 'full' === $query_args['refunds'] || 'partial' === $query_args['refunds'] ) {
$operator = 'full' === $query_args['refunds'] ? '=' : '!=';
$sql_query['from_clause'] .= " JOIN {$table_name} parent_order_stats ON {$table_name}.parent_id = parent_order_stats.order_id";
$sql_query['where_clause'] .= "parent_order_stats.status {$operator} '{$this->normalize_order_status( 'refunded' )}'";
* Returns an array of products belonging to given categories.
* @param array $categories List of categories IDs.
protected function get_products_by_cat_ids( $categories ) {
'taxonomy' => 'product_cat',
'include' => $categories,
if ( is_wp_error( $terms ) || empty( $terms ) ) {
'category' => wc_list_pluck( $terms, 'slug' ),
return wc_get_products( $args );
* Get WHERE filter by object ids subquery.
* @param string $select_table Select table name.
* @param string $select_field Select table object ID field name.
* @param string $filter_table Lookup table name.
* @param string $filter_field Lookup table object ID field name.
* @param string $compare Comparison string (IN|NOT IN).
* @param string $id_list Comma separated ID list.
protected function get_object_where_filter( $select_table, $select_field, $filter_table, $filter_field, $compare, $id_list ) {
if ( empty( $id_list ) ) {
$lookup_name = isset( $wpdb->$filter_table ) ? $wpdb->$filter_table : $wpdb->prefix . $filter_table;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return " {$select_table}.{$select_field} {$compare} (
DISTINCT {$filter_table}.{$select_field}
{$filter_table}.{$filter_field} IN ({$id_list})
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
* Returns an array of ids of allowed products, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_included_products_array( $query_args ) {
$included_products = array();
$operator = $this->get_match_operator( $query_args );
if ( isset( $query_args['category_includes'] ) && is_array( $query_args['category_includes'] ) && count( $query_args['category_includes'] ) > 0 ) {
$included_products = $this->get_products_by_cat_ids( $query_args['category_includes'] );
// If no products were found in the specified categories, we will force an empty set
// by matching a product ID of -1, unless the filters are OR/any and products are specified.
if ( empty( $included_products ) ) {
$included_products = array( '-1' );
if ( isset( $query_args['product_includes'] ) && is_array( $query_args['product_includes'] ) && count( $query_args['product_includes'] ) > 0 ) {
if ( count( $included_products ) > 0 ) {
if ( 'AND' === $operator ) {
// AND results in an intersection between products from selected categories and manually included products.
$included_products = array_intersect( $included_products, $query_args['product_includes'] );
} elseif ( 'OR' === $operator ) {
// OR results in a union of products from selected categories and manually included products.
$included_products = array_merge( $included_products, $query_args['product_includes'] );
$included_products = $query_args['product_includes'];
return $included_products;
* Returns comma separated ids of allowed products, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_included_products( $query_args ) {
$included_products = $this->get_included_products_array( $query_args );
return implode( ',', $included_products );
* Returns comma separated ids of allowed variations, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_included_variations( $query_args ) {
return $this->get_filtered_ids( $query_args, 'variation_includes' );
* Returns comma separated ids of excluded variations, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_excluded_variations( $query_args ) {
return $this->get_filtered_ids( $query_args, 'variation_excludes' );
* Returns an array of ids of disallowed products, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_excluded_products_array( $query_args ) {
$excluded_products = array();
$operator = $this->get_match_operator( $query_args );
if ( isset( $query_args['category_excludes'] ) && is_array( $query_args['category_excludes'] ) && count( $query_args['category_excludes'] ) > 0 ) {
$excluded_products = $this->get_products_by_cat_ids( $query_args['category_excludes'] );
if ( isset( $query_args['product_excludes'] ) && is_array( $query_args['product_excludes'] ) && count( $query_args['product_excludes'] ) > 0 ) {
$excluded_products = array_merge( $excluded_products, $query_args['product_excludes'] );
return $excluded_products;
* Returns comma separated ids of excluded products, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_excluded_products( $query_args ) {
$excluded_products = $this->get_excluded_products_array( $query_args );
return implode( ',', $excluded_products );
* Returns comma separated ids of included categories, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_included_categories( $query_args ) {
return $this->get_filtered_ids( $query_args, 'category_includes' );
* Returns comma separated ids of included coupons, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
* @param string $field Field name in the parameter list.
protected function get_included_coupons( $query_args, $field = 'coupon_includes' ) {
return $this->get_filtered_ids( $query_args, $field );
* Returns comma separated ids of excluded coupons, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_excluded_coupons( $query_args ) {
return $this->get_filtered_ids( $query_args, 'coupon_excludes' );
* Returns comma separated ids of included orders, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_included_orders( $query_args ) {
return $this->get_filtered_ids( $query_args, 'order_includes' );
* Returns comma separated ids of excluded orders, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_excluded_orders( $query_args ) {
return $this->get_filtered_ids( $query_args, 'order_excludes' );
* Returns comma separated ids of included users, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_included_users( $query_args ) {
return $this->get_filtered_ids( $query_args, 'user_includes' );
* Returns comma separated ids of excluded users, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_excluded_users( $query_args ) {
return $this->get_filtered_ids( $query_args, 'user_excludes' );
* Returns order status subquery to be used in WHERE SQL query, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
* @param string $operator AND or OR, based on match query argument.
protected function get_status_subquery( $query_args, $operator = 'AND' ) {
$excluded_statuses = array();
if ( isset( $query_args['status_is'] ) && is_array( $query_args['status_is'] ) && count( $query_args['status_is'] ) > 0 ) {
$allowed_statuses = array_map( array( $this, 'normalize_order_status' ), esc_sql( $query_args['status_is'] ) );
if ( $allowed_statuses ) {
$subqueries[] = "{$wpdb->prefix}wc_order_stats.status IN ( '" . implode( "','", $allowed_statuses ) . "' )";
if ( isset( $query_args['status_is_not'] ) && is_array( $query_args['status_is_not'] ) && count( $query_args['status_is_not'] ) > 0 ) {
$excluded_statuses = array_map( array( $this, 'normalize_order_status' ), $query_args['status_is_not'] );
if ( ( ! isset( $query_args['status_is'] ) || empty( $query_args['status_is'] ) )
&& ( ! isset( $query_args['status_is_not'] ) || empty( $query_args['status_is_not'] ) )
$excluded_statuses = array_map( array( $this, 'normalize_order_status' ), $this->get_excluded_report_order_statuses() );
if ( $excluded_statuses ) {
$subqueries[] = "{$wpdb->prefix}wc_order_stats.status NOT IN ( '" . implode( "','", $excluded_statuses ) . "' )";
return implode( " $operator ", $subqueries );
* Add order status SQL clauses if included in query.
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Database table name.
* @param SqlQuery $sql_query Query object.
protected function add_order_status_clause( $query_args, $table_name, &$sql_query ) {
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$sql_query->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
$sql_query->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
* Add order by SQL clause if included in query.
* @param array $query_args Parameters supplied by the user.
* @param SqlQuery $sql_query Query object.
* @return string Order by clause.
protected function add_order_by_clause( $query_args, &$sql_query ) {
$sql_query->clear_sql_clause( array( 'order_by' ) );
if ( isset( $query_args['orderby'] ) ) {
$order_by_clause = $this->normalize_order_by( esc_sql( $query_args['orderby'] ) );
$sql_query->add_sql_clause( 'order_by', $order_by_clause );
// Return ORDER BY clause to allow adding the sort field(s) to query via a JOIN.
* Add order by order SQL clause.
* @param array $query_args Parameters supplied by the user.
* @param SqlQuery $sql_query Query object.
protected function add_orderby_order_clause( $query_args, &$sql_query ) {
if ( isset( $query_args['order'] ) ) {
$sql_query->add_sql_clause( 'order_by', esc_sql( $query_args['order'] ) );
$sql_query->add_sql_clause( 'order_by', 'DESC' );
* Returns customer subquery to be used in WHERE SQL query, based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_customer_subquery( $query_args ) {
if ( isset( $query_args['customer_type'] ) ) {
if ( 'new' === strtolower( $query_args['customer_type'] ) ) {
$customer_filter = " {$wpdb->prefix}wc_order_stats.returning_customer = 0";
} elseif ( 'returning' === strtolower( $query_args['customer_type'] ) ) {
$customer_filter = " {$wpdb->prefix}wc_order_stats.returning_customer = 1";
* Returns product attribute subquery elements used in JOIN and WHERE clauses,
* based on query arguments from the user.
* @param array $query_args Parameters supplied by the user.
protected function get_attribute_subqueries( $query_args ) {
$match_operator = $this->get_match_operator( $query_args );
$post_meta_comparators = array(
'!=' => 'attribute_is_not',
foreach ( $post_meta_comparators as $comparator => $arg ) {
if ( ! isset( $query_args[ $arg ] ) || ! is_array( $query_args[ $arg ] ) ) {
foreach ( $query_args[ $arg ] as $attribute_term ) {
if ( ! is_array( $attribute_term ) || 2 !== count( $attribute_term ) ) {
// If the tuple is numeric, assume these are IDs.
if ( is_numeric( $attribute_term[0] ) && is_numeric( $attribute_term[1] ) ) {
$attribute_id = intval( $attribute_term[0] );
$term_id = intval( $attribute_term[1] );
if ( 0 === $attribute_id || 0 === $term_id ) {
// @todo: Use wc_get_attribute () instead ?
$attr_taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );
if ( empty( $attr_taxonomy ) ) {
$attr_term = get_term_by( 'id', $term_id, $attr_taxonomy );
if ( false === $attr_term ) {
$meta_key = sanitize_title( $attr_taxonomy );
$meta_value = $attr_term->slug;
// Assume these are a custom attribute slug/value pair.
$meta_key = esc_sql( $attribute_term[0] );
$meta_value = esc_sql( $attribute_term[1] );
$attr_term = get_term_by( 'slug', $meta_value, $meta_key );
if ( false !== $attr_term ) {
$term_id = $attr_term->term_id;