* Base Report data store utility functions - 1

Standardizing the data store classes will be easier if the base class contains simple utility functions
that can replace logic implemented in multiple data stores.

- set_db_table_name() assigns a WP DB table name class variable for the data store
- get_db_table_name() retrieves the WP DB table name
- prepend_table_name() prepends a field in a query fragment with the data store table name

* add context, clause handling, and filters to reports data store

* add generated SQL clauses to class properties

* reduce id getter logic to single function with filter, add phpdocs to new filters

* update table_name to private string for use in constructor

* extract SQL query clause handling to its own class

- Will allow for use in subquery processing without creating a get_data stub
- Swap parameter order in add_sql_clause for readability
- Add support for clearing multple clauses in one call

* add context var to SqlQuery class

* implement SqlQuery in Categories data store

* implement subquery in categories data store

* coupons data stores, more underlying refactor

- fix warnings
- make filtered id functions static
- add limit parameter handling
- update coupons data store
- update coupon stats data store

* refactor coupon stats data store

* refactor customers and customer stats data stores

* add context to subqueries

* add missed prepend table name call

* refactor downloads data store, fix some warnings

* fix warnings, add separator parameter to filtered IDs

* refactor taxes and tax stats data stores

* refactor variations data store

* refactor product and product stats data stores

* make table_name static throughout for compat with static hook functions

* refactor order and order stats datastores

- use consistent visibility on initialize_queries()
- update db_table_name logic to use static keyword instead of self

* fix missed whitespace

* fix segmenting query, add SqlQuery join clause

* DRY data store constructors, class properties

* prefix table name when not yet assigned

* fix unit tests, interpolations, WPDB delete calls

* DRY get_object_where_filter()

* remove redundant table prefix from unit test init

* fix refactored SQL queries

* restore product paging

* remove unused query param arrays

* add first pass on data docs readme

* remove debug code, errant SQL spacing

* refactor out outer_from query element

* merge wheres, joins before filtering

* move all report column definitions to assign_report_columns

* fix data readme markdown

* small code formating fixes from review

* remove static from query/datastore context

* missed self:: in previous, add comments, small code moves

* rename get_statement() to get_query_statement()

* remove temporary query references

* static reference, remove reference parameter, fix coupon compare

* add todo reminders

* use correct query parameter in coupon data stores
This commit is contained in:
Ron Rennick 2019-11-07 13:28:37 -04:00 committed by Jeff Stieler
parent 9a9d812e60
commit afed4fba36
21 changed files with 1677 additions and 1492 deletions

View File

@ -1,4 +1,84 @@
Data
====
TBA, this will document our data implementation.
WooCommerce Admin data stores implement the [`SqlQuery` class](https://github.com/woocommerce/woocommerce-admin/blob/master/src/API/Reports/SqlQuery.php).
### SqlQuery Class
The `SqlQuery` class is a SQL Query statement object. Its properties consist of
- A `context` string identifying the context of the query.
- SQL clause (`type`) string arrays used to construct the SQL statement:
- `select`
- `from`
- `right_join`
- `join`
- `left_join`
- `where`
- `where_time`
- `group_by`
- `having`
- `order_by`
- `limit`
### Reports Data Stores
The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends the `SqlQuery` class. The implementation data store classes use the following `SqlQuery` instances:
| Data Store | Context | Class Query | Sub Query | Interval Query | Total Query |
| ---------- | ------- | ----------- | --------- | -------------- | ----------- |
| Categories | categories | Yes | Yes | - | - |
| Coupons | coupons | Yes | Yes | - | - |
| Coupon Stats | coupon_stats | Yes | - | Yes | Yes |
| Customers | customers | Yes | Yes | - | - |
| Customer Stats | customer_stats | Yes | - | Yes | Yes |
| Downloads | downloads | Yes | Yes | - | - |
| Download Stats | download_stats | Yes | - | Yes | Yes |
| Orders | orders | Yes | Yes | - | - |
| Order Stats | order_stats | Yes | - | Yes | Yes |
| Products | products | Yes | Yes | - | - |
| Product Stats | product_stats | Yes | - | Yes | Yes |
| Taxes | taxes | Yes | Yes | - | - |
| Tax Stats | tax_stats | Yes | - | Yes | Yes |
| Variations | variations | Yes | Yes | - | - |
Query contexts are named as follows:
- Class Query = Class Context
- Sub Query = Class Context + `_subquery`
- Interval Query = Class Context + `_interval`
- Total Query = Class Context + `_total`
### Filters
When getting the full statement the clause arrays are passed through two filters where `$context` is the query object context and `$type` is:
- `select`
- `from`
- `join` = `right_join` + `join` + `left_join`
- `where` = `where` + `where_time`
- `group_by`
- `having`
- `order_by`
- `limit`
The filters are:
- `apply_filters( "wc_admin_clauses_{$type}", $clauses, $context );`
- `apply_filters( "wc_admin_clauses_{$type}_{$context}", $clauses );`
Example usage
```
add_filter( 'wc_admin_clauses_product_stats_select_total', 'my_custom_product_stats' );
/**
* Add sample data to product stats totals.
*
* @param array $clauses array of SELECT clauses.
* @return array
*/
function my_custom_product_stats( $clauses ) {
$clauses[] = ', SUM( sample_column ) as sample_total';
return $clauses;
}
```

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Categories\DataStore.
@ -23,7 +24,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_order_product_lookup';
protected static $table_name = 'wc_order_product_lookup';
/**
* Cache identifier.
@ -60,102 +61,83 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* SQL columns to select in the db query.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
);
protected $context = 'categories';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Avoid ambigious column order_id in SQL query.
$this->report_columns['products_count'] = str_replace( 'product_id', $table_name . '.product_id', $this->report_columns['products_count'] );
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
'products_count' => "COUNT(DISTINCT {$table_name}.product_id) as products_count",
);
}
/**
* Return the database query with parameters used for Categories report: time span and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$order_product_lookup_table = self::get_db_table_name();
$sql_query_params = $this->get_time_period_sql_params( $query_args, $order_product_lookup_table );
$this->get_time_period_sql_params( $query_args, $order_product_lookup_table );
// join wp_order_product_lookup_table with relationships and taxonomies.
$sql_query_params['from_clause'] .= " LEFT JOIN {$wpdb->term_relationships} ON {$order_product_lookup_table}.product_id = {$wpdb->term_relationships}.object_id";
$sql_query_params['from_clause'] .= " LEFT JOIN {$wpdb->wc_category_lookup} ON {$wpdb->term_relationships}.term_taxonomy_id = {$wpdb->wc_category_lookup}.category_id";
// join wp_order_product_lookup_table with relationships and taxonomies
// @todo How to handle custom product tables?
$this->subquery->add_sql_clause( 'left_join', "LEFT JOIN {$wpdb->term_relationships} ON {$order_product_lookup_table}.product_id = {$wpdb->term_relationships}.object_id" );
$this->subquery->add_sql_clause( 'left_join', "LEFT JOIN {$wpdb->wc_category_lookup} ON {$wpdb->term_relationships}.term_taxonomy_id = {$wpdb->wc_category_lookup}.category_id" );
$included_categories = $this->get_included_categories( $query_args );
if ( $included_categories ) {
$sql_query_params['where_clause'] .= " AND {$wpdb->wc_category_lookup}.category_tree_id IN ({$included_categories})";
$this->subquery->add_sql_clause( 'where', "AND {$wpdb->wc_category_lookup}.category_tree_id IN ({$included_categories})" );
// Limit is left out here so that the grouping in code by PHP can be applied correctly.
// This also needs to be put after the term_taxonomy JOIN so that we can match the correct term name.
$sql_query_params = $this->get_order_by_params( $query_args, $sql_query_params, 'outer_from_clause', 'default_results.category_id' );
$this->get_order_by_params( $query_args, 'outer', 'default_results.category_id' );
} else {
$sql_query_params = $this->get_order_by_params( $query_args, $sql_query_params, 'from_clause', "{$wpdb->wc_category_lookup}.category_tree_id" );
$this->get_order_by_params( $query_args, 'inner', "{$wpdb->wc_category_lookup}.category_tree_id" );
}
// @todo Only products in the category C or orders with products from category C (and, possibly others?).
$included_products = $this->get_included_products( $query_args );
if ( $included_products ) {
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )";
}
$sql_query_params['where_clause'] .= " AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL";
return $sql_query_params;
$this->add_order_status_clause( $query_args, $order_product_lookup_table, $this->subquery );
$this->subquery->add_sql_clause( 'where', "AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL" );
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param array $sql_query Current SQL query array.
* @param string $from_arg Name of the FROM sql param.
* @param string $from_arg Target of the JOIN sql param.
* @param string $id_cell ID cell identifier, like `table_name.id_column_name`.
* @return array
*/
protected function get_order_by_params( $query_args, $sql_query, $from_arg, $id_cell ) {
protected function get_order_by_params( $query_args, $from_arg, $id_cell ) {
global $wpdb;
$lookup_table = $wpdb->prefix . self::TABLE_NAME;
$lookup_table = self::get_db_table_name();
$order_by_clause = $this->add_order_by_clause( $query_args, $this );
$this->add_orderby_order_clause( $query_args, $this );
$sql_query['order_by_clause'] = '';
if ( isset( $query_args['orderby'] ) ) {
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
if ( false !== strpos( $order_by_clause, '_terms' ) ) {
$join = "JOIN {$wpdb->terms} AS _terms ON {$id_cell} = _terms.term_id";
if ( 'inner' === $from_arg ) {
$this->subquery->add_sql_clause( 'join', $join );
} else {
$this->add_sql_clause( 'join', $join );
}
}
$sql_query['outer_from_clause'] = '';
if ( false !== strpos( $sql_query['order_by_clause'], '_terms' ) ) {
$sql_query[ $from_arg ] .= " JOIN {$wpdb->prefix}terms AS _terms ON {$id_cell} = _terms.term_id";
}
if ( isset( $query_args['order'] ) ) {
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
} else {
$sql_query['order_by_clause'] .= ' DESC';
}
return $sql_query;
}
/**
@ -187,17 +169,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return array();
}
/**
* Returns comma separated ids of included categories, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_categories( $query_args ) {
$included_categories = $this->get_included_categories_array( $query_args );
return implode( ',', $included_categories );
}
/**
* Returns the page of data according to page number and items per page.
*
@ -236,7 +207,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -261,6 +232,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -268,45 +241,31 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$included_categories = $this->get_included_categories_array( $query_args );
$this->get_sql_query_params( $query_args );
if ( count( $included_categories ) > 0 ) {
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( array_merge( array( 'category_id' ), $fields ), array( 'category_id' ) );
$ids_table = $this->get_ids_table( $included_categories, 'category_id' );
$fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_categories, 'category_id' );
$prefix = "SELECT {$join_selections} FROM (";
$suffix = ") AS {$table_name}";
$right_join = "RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.category_id = {$table_name}.category_id";
$this->add_sql_clause( 'select', $this->format_join_selections( array_merge( array( 'category_id' ), $fields ), array( 'category_id' ) ) );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.category_id = {$table_name}.category_id"
);
$categories_query = $this->get_query_statement();
} else {
$prefix = '';
$suffix = '';
$right_join = '';
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$categories_query = $this->subquery->get_query_statement();
}
$categories_data = $wpdb->get_results(
"${prefix}
SELECT
{$wpdb->wc_category_lookup}.category_tree_id as category_id,
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
{$wpdb->wc_category_lookup}.category_tree_id
{$suffix}
{$right_join}
{$sql_query_params['outer_from_clause']}
ORDER BY
{$sql_query_params['order_by_clause']}
",
$categories_query,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -335,4 +294,15 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return $data;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
global $wpdb;
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', "{$wpdb->wc_category_lookup}.category_tree_id as category_id," );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', "{$wpdb->wc_category_lookup}.category_tree_id" );
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
@ -24,7 +25,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_order_coupon_lookup';
protected static $table_name = 'wc_order_coupon_lookup';
/**
* Cache identifier.
@ -45,24 +46,22 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array(
'coupon_id' => 'coupon_id',
'amount' => 'SUM(discount_amount) as amount',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
);
protected $context = 'coupons';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Avoid ambigious column order_id in SQL query.
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'coupon_id' => 'coupon_id',
'amount' => 'SUM(discount_amount) as amount',
'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
);
}
/**
@ -86,78 +85,55 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return array();
}
/**
* Returns comma separated ids of included coupons, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_coupons( $query_args ) {
$included_coupons = $this->get_included_coupons_array( $query_args );
return implode( ',', $included_coupons );
}
/**
* Updates the database query with parameters used for Products report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$order_coupon_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$order_coupon_lookup_table = self::get_db_table_name();
$sql_query_params = $this->get_time_period_sql_params( $query_args, $order_coupon_lookup_table );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$this->get_time_period_sql_params( $query_args, $order_coupon_lookup_table );
$this->get_limit_sql_params( $query_args );
$included_coupons = $this->get_included_coupons( $query_args );
$included_coupons = $this->get_included_coupons( $query_args, 'coupons' );
if ( $included_coupons ) {
$sql_query_params['where_clause'] .= " AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})";
$this->subquery->add_sql_clause( 'where', "AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})" );
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_params( $query_args, 'outer_from_clause', 'default_results.coupon_id' ) );
$this->get_order_by_params( $query_args, 'outer', 'default_results.coupon_id' );
} else {
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_params( $query_args, 'from_clause', "{$order_coupon_lookup_table}.coupon_id" ) );
$this->get_order_by_params( $query_args, 'inner', "{$order_coupon_lookup_table}.coupon_id" );
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_coupon_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )";
}
return $sql_query_params;
$this->add_order_status_clause( $query_args, $order_coupon_lookup_table, $this->subquery );
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param string $from_arg Name of the FROM sql param.
* @param string $from_arg Target of the JOIN sql param.
* @param string $id_cell ID cell identifier, like `table_name.id_column_name`.
* @return array
*/
protected function get_order_by_params( $query_args, $from_arg, $id_cell ) {
global $wpdb;
$lookup_table = $wpdb->prefix . self::TABLE_NAME;
$sql_query = array();
$sql_query['from_clause'] = '';
$sql_query['outer_from_clause'] = '';
$sql_query['order_by_clause'] = '';
if ( isset( $query_args['orderby'] ) ) {
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
}
$lookup_table = self::get_db_table_name();
$order_by_clause = $this->add_order_by_clause( $query_args, $this );
$join = "JOIN {$wpdb->posts} AS _coupons ON {$id_cell} = _coupons.ID";
$this->add_orderby_order_clause( $query_args, $this );
if ( false !== strpos( $sql_query['order_by_clause'], '_coupons' ) ) {
$sql_query[ $from_arg ] .= " JOIN {$wpdb->prefix}posts AS _coupons ON {$id_cell} = _coupons.ID";
}
if ( isset( $query_args['order'] ) ) {
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
if ( 'inner' === $from_arg ) {
$this->subquery->clear_sql_clause( 'join' );
if ( false !== strpos( $order_by_clause, '_coupons' ) ) {
$this->subquery->add_sql_clause( 'join', $join );
}
} else {
$sql_query['order_by_clause'] .= ' DESC';
$this->clear_sql_clause( 'join' );
if ( false !== strpos( $order_by_clause, '_coupons' ) ) {
$this->add_sql_clause( 'join', $join );
}
}
return $sql_query;
}
/**
@ -235,7 +211,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -260,6 +236,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -268,69 +246,51 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );
$included_coupons = $this->get_included_coupons_array( $query_args );
$limit_params = $this->get_limit_params( $query_args );
$this->subquery->add_sql_clause( 'select', $selections );
$this->get_sql_query_params( $query_args );
if ( count( $included_coupons ) > 0 ) {
$total_results = count( $included_coupons );
$total_pages = (int) ceil( $total_results / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $total_results / $limit_params['per_page'] );
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, array( 'coupon_id' ) );
$ids_table = $this->get_ids_table( $included_coupons, 'coupon_id' );
$fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_coupons, 'coupon_id' );
$prefix = "SELECT {$join_selections} FROM (";
$suffix = ") AS {$table_name}";
$right_join = "RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.coupon_id = {$table_name}.coupon_id";
$this->add_sql_clause( 'select', $this->format_join_selections( $fields, array( 'coupon_id' ) ) );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.coupon_id = {$table_name}.coupon_id"
);
$coupons_query = $this->get_query_statement();
} else {
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$coupons_query = $this->subquery->get_query_statement();
$this->subquery->clear_sql_clause( array( 'select', 'order_by' ) );
$this->subquery->add_sql_clause( 'select', 'coupon_id' );
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT
coupon_id
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
coupon_id
) AS tt"
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $db_records_count / $limit_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$prefix = '';
$suffix = '';
$right_join = '';
}
$coupon_data = $wpdb->get_results(
"${prefix}
SELECT
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
coupon_id
{$suffix}
{$right_join}
{$sql_query_params['outer_from_clause']}
ORDER BY
{$sql_query_params['order_by_clause']}
{$sql_query_params['limit']}
",
$coupons_query,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -388,7 +348,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
$result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME,
self::get_db_table_name(),
array(
'order_id' => $order_id,
'coupon_id' => $coupon_id,
@ -426,15 +386,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public static function sync_on_order_delete( $order_id ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM ${table_name} WHERE order_id = %d",
$order_id
)
);
$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
/**
* Fires when coupon's reports are removed from database.
*
@ -460,7 +412,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
$wpdb->delete(
$wpdb->prefix . self::TABLE_NAME,
self::get_db_table_name(),
array( 'coupon_id' => $post_id )
);
@ -474,15 +426,25 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* @param array $args Array of args to filter the query by. Supports `include`.
* @return array Array of results.
*/
public static function get_coupons( $args ) {
public function get_coupons( $args ) {
global $wpdb;
$query = "SELECT ID, post_title FROM {$wpdb->prefix}posts WHERE post_type='shop_coupon'";
$query = "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_type='shop_coupon'";
if ( ! empty( $args['include'] ) ) {
$included_coupons = implode( ',', $args['include'] );
$query .= " AND ID IN ({$included_coupons})";
$included_coupons = $this->get_included_coupons( $args, 'include' );
if ( ! empty( $included_coupons ) ) {
$query .= " AND ID IN ({$included_coupons})";
}
return $wpdb->get_results( $query ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', 'coupon_id' );
}
}

View File

@ -11,6 +11,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Coupons\Stats\DataStore.
@ -36,11 +37,14 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
*
* @var array
*/
protected $report_columns = array(
'amount' => 'SUM(discount_amount) as amount',
'coupons_count' => 'COUNT(DISTINCT coupon_id) as coupons_count',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
);
protected $report_columns;
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'coupon_stats';
/**
* Cache identifier.
@ -50,48 +54,56 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
protected $cache_key = 'coupons_stats';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Avoid ambigious column order_id in SQL query.
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'amount' => 'SUM(discount_amount) as amount',
'coupons_count' => 'COUNT(DISTINCT coupon_id) as coupons_count',
'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
);
}
/**
* Updates the database query with parameters used for Products Stats report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @param array $totals_params SQL parameters for the totals query.
* @param array $intervals_params SQL parameters for the intervals query.
*/
protected function update_sql_query_params( $query_args, &$totals_params, &$intervals_params ) {
protected function update_sql_query_params( $query_args ) {
global $wpdb;
$coupons_where_clause = '';
$coupons_from_clause = '';
$clauses = array(
'where' => '',
'join' => '',
);
$order_coupon_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$order_coupon_lookup_table = self::get_db_table_name();
$included_coupons = $this->get_included_coupons( $query_args );
$included_coupons = $this->get_included_coupons( $query_args, 'coupons' );
if ( $included_coupons ) {
$coupons_where_clause .= " AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})";
$clauses['where'] .= " AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})";
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$coupons_from_clause .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_coupon_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$coupons_where_clause .= " AND ( {$order_status_filter} )";
$clauses['join'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_coupon_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$clauses['where'] .= " AND ( {$order_status_filter} )";
}
$totals_params = array_merge( $totals_params, $this->get_time_period_sql_params( $query_args, $order_coupon_lookup_table ) );
$totals_params['where_clause'] .= $coupons_where_clause;
$totals_params['from_clause'] .= $coupons_from_clause;
$this->get_time_period_sql_params( $query_args, $order_coupon_lookup_table );
$this->get_intervals_sql_params( $query_args, $order_coupon_lookup_table );
$clauses['where_time'] = $this->get_sql_clause( 'where_time' );
$intervals_params = array_merge( $intervals_params, $this->get_intervals_sql_params( $query_args, $order_coupon_lookup_table ) );
$intervals_params['where_clause'] .= $coupons_where_clause;
$intervals_params['from_clause'] .= $coupons_from_clause;
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) );
$this->interval_query->add_sql_clause( 'select', 'AS time_interval' );
foreach ( array( 'join', 'where_time', 'where' ) as $clause ) {
$this->interval_query->add_sql_clause( $clause, $clauses[ $clause ] );
$this->total_query->add_sql_clause( $clause, $clauses[ $clause ] );
}
}
/**
@ -104,7 +116,7 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -129,6 +141,8 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -139,73 +153,57 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
$selections = $this->selected_columns( $query_args );
$totals_query = array();
$intervals_query = array();
$limit_params = $this->get_limit_sql_params( $query_args );
$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
$db_intervals = $wpdb->get_col(
"SELECT
{$intervals_query['select_clause']} AS time_interval
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval"
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$db_interval_count = count( $db_intervals );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] );
$total_pages = (int) ceil( $expected_interval_count / $limit_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->total_query->add_sql_clause( 'select', $selections );
$totals = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}",
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return $data;
}
// @todo remove these assignements when refactoring segmenter classes to use query objects.
$totals_query = array(
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
);
$intervals_query = array(
'select_clause' => $this->get_sql_clause( 'select' ),
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$segmenter = new Segmenter( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$totals = (object) $this->cast_numbers( $totals[0] );
// Intervals.
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$selections = ', ' . $selections;
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$intervals = $wpdb->get_results(
"SELECT
MAX({$table_name}.date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval
{$selections}
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval
ORDER BY
{$intervals_query['order_by_clause']}
{$intervals_query['limit']}",
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -221,10 +219,10 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
'page_no' => (int) $query_args['page'],
);
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $limit_params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $limit_params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}
@ -236,4 +234,18 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
return $data;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
@ -24,7 +25,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_customer_lookup';
protected static $table_name = 'wc_customer_lookup';
/**
* Cache identifier.
@ -47,36 +48,35 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array(
'id' => 'customer_id as id',
'user_id' => 'user_id',
'username' => 'username',
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL?
'email' => 'email',
'country' => 'country',
'city' => 'city',
'state' => 'state',
'postcode' => 'postcode',
'date_registered' => 'date_registered',
'date_last_active' => 'IF( date_last_active <= "0000-00-00 00:00:00", NULL, date_last_active ) AS date_last_active',
'orders_count' => 'SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) as orders_count',
'total_spend' => 'SUM( gross_total ) as total_spend',
'avg_order_value' => '( SUM( gross_total ) / COUNT( order_id ) ) as avg_order_value',
);
protected $context = 'customers';
/**
* Constructor.
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
protected function assign_report_columns() {
global $wpdb;
// Initialize some report columns that need disambiguation.
$this->report_columns['id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id as id';
$this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order";
$table_name = self::get_db_table_name();
$this->report_columns = array(
'id' => "{$table_name}.customer_id as id",
'user_id' => 'user_id',
'username' => 'username',
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL?
'email' => 'email',
'country' => 'country',
'city' => 'city',
'state' => 'state',
'postcode' => 'postcode',
'date_registered' => 'date_registered',
'date_last_active' => 'IF( date_last_active <= "0000-00-00 00:00:00", NULL, date_last_active ) AS date_last_active',
'date_last_order' => "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order",
'orders_count' => 'SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) as orders_count',
'total_spend' => 'SUM( gross_total ) as total_spend',
'avg_order_value' => '( SUM( gross_total ) / COUNT( order_id ) ) as avg_order_value',
);
}
/**
@ -115,43 +115,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return $order_by;
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_order_by_sql_params( $query_args ) {
$sql_query['order_by_clause'] = '';
if ( isset( $query_args['orderby'] ) ) {
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
}
if ( isset( $query_args['order'] ) ) {
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
} else {
$sql_query['order_by_clause'] .= ' DESC';
}
return $sql_query;
}
/**
* Fills WHERE clause of SQL request with date-related constraints.
*
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint.
* @return array
*/
protected function get_time_period_sql_params( $query_args, $table_name ) {
global $wpdb;
$sql_query = array(
'where_time_clause' => '',
'where_clause' => '',
'having_clause' => '',
);
$this->clear_sql_clause( array( 'where', 'where_time', 'having' ) );
$date_param_mapping = array(
'registered' => array(
'clause' => 'where',
@ -202,31 +175,28 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
if ( $where_time_clauses ) {
$sql_query['where_time_clause'] = ' AND ' . implode( " {$match_operator} ", $where_time_clauses );
$this->subquery->add_sql_clause( 'where_time', 'AND ' . implode( " {$match_operator} ", $where_time_clauses ) );
}
if ( $having_time_clauses ) {
$sql_query['having_clause'] = ' AND ' . implode( " {$match_operator} ", $having_time_clauses );
$this->subquery->add_sql_clause( 'having', 'AND ' . implode( " {$match_operator} ", $having_time_clauses ) );
}
return $sql_query;
}
/**
* Updates the database query with parameters used for Customers report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$customer_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$customer_lookup_table = self::get_db_table_name();
$order_stats_table_name = $wpdb->prefix . 'wc_order_stats';
$sql_query_params = $this->get_time_period_sql_params( $query_args, $customer_lookup_table );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
$sql_query_params['from_clause'] = " LEFT JOIN {$order_stats_table_name} ON {$customer_lookup_table}.customer_id = {$order_stats_table_name}.customer_id";
$this->get_time_period_sql_params( $query_args, $customer_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->get_order_by_sql_params( $query_args );
$this->subquery->add_sql_clause( 'left_join', "LEFT JOIN {$order_stats_table_name} ON {$customer_lookup_table}.customer_id = {$order_stats_table_name}.customer_id" );
$match_operator = $this->get_match_operator( $query_args );
$where_clauses = array();
@ -279,7 +249,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
// Allow a list of customer IDs to be specified.
if ( ! empty( $query_args['customers'] ) ) {
$included_customers = implode( ',', array_map( 'intval', $query_args['customers'] ) );
$included_customers = $this->get_filtered_ids( $query_args, 'customers' );
$where_clauses[] = "{$customer_lookup_table}.customer_id IN ({$included_customers})";
}
@ -324,21 +294,19 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
if ( $where_clauses ) {
$preceding_match = empty( $sql_query_params['where_time_clause'] ) ? ' AND ' : " {$match_operator} ";
$sql_query_params['where_clause'] = $preceding_match . implode( " {$match_operator} ", $where_clauses );
$preceding_match = empty( $this->get_sql_clause( 'where_time' ) ) ? ' AND ' : " {$match_operator} ";
$this->subquery->add_sql_clause( 'where', $preceding_match . implode( " {$match_operator} ", $where_clauses ) );
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$sql_query_params['from_clause'] .= " AND ( {$order_status_filter} )";
$this->subquery->add_sql_clause( 'left_join', "AND ( {$order_status_filter} )" );
}
if ( $having_clauses ) {
$preceding_match = empty( $sql_query_params['having_clause'] ) ? ' AND ' : " {$match_operator} ";
$sql_query_params['having_clause'] .= $preceding_match . implode( " {$match_operator} ", $having_clauses );
$preceding_match = empty( $this->get_sql_clause( 'having' ) ) ? ' AND ' : " {$match_operator} ";
$this->subquery->add_sql_clause( 'having', $preceding_match . implode( " {$match_operator} ", $having_clauses ) );
}
return $sql_query_params;
}
/**
@ -350,7 +318,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$customers_table_name = $wpdb->prefix . self::TABLE_NAME;
$customers_table_name = self::get_db_table_name();
$order_stats_table_name = $wpdb->prefix . 'wc_order_stats';
// These defaults are only partially applied when used via REST API, as that has its own defaults.
@ -374,6 +342,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -386,46 +356,23 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT {$customers_table_name}.customer_id
FROM
{$customers_table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
{$customers_table_name}.customer_id
HAVING
1=1
{$sql_query_params['having_clause']}
{$this->subquery->get_query_statement()}
) as tt
"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
$params = $this->get_limit_params( $query_args );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$customer_data = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$customers_table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
{$customers_table_name}.customer_id
HAVING
1=1
{$sql_query_params['having_clause']}
ORDER BY
{$sql_query_params['order_by_clause']}
{$sql_query_params['limit']}
",
$this->subquery->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -542,7 +489,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$format[] = '%s';
}
$result = $wpdb->insert( $wpdb->prefix . self::TABLE_NAME, $data, $format );
$result = $wpdb->insert( self::get_db_table_name(), $data, $format );
$customer_id = $wpdb->insert_id;
/**
@ -564,7 +511,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public static function get_guest_id_by_email( $email ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
$customer_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT customer_id FROM {$table_name} WHERE email = %s AND user_id IS NULL LIMIT 1",
@ -584,7 +531,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public static function get_customer_id_by_user_id( $user_id ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
$customer_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT customer_id FROM {$table_name} WHERE user_id = %d LIMIT 1",
@ -680,7 +627,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$format[] = '%d';
}
$results = $wpdb->replace( $wpdb->prefix . self::TABLE_NAME, $data, $format );
$results = $wpdb->replace( self::get_db_table_name(), $data, $format );
/**
* Fires when customser's reports are updated.
@ -723,14 +670,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public static function delete_customer( $customer_id ) {
global $wpdb;
$customer_id = (int) $customer_id;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM ${table_name} WHERE customer_id = %d",
$customer_id
)
);
$wpdb->delete( self::get_db_table_name(), array( 'customer_id' => $customer_id ) );
/**
* Fires when a customer is deleted.
@ -739,4 +680,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*/
do_action( 'woocommerce_reports_delete_customer', $customer_id );
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$table_name = self::get_db_table_name();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'from', $table_name );
$this->subquery->add_sql_clause( 'select', "{$table_name}.customer_id" );
$this->subquery->add_sql_clause( 'group_by', "{$table_name}.customer_id" );
}
}

View File

@ -28,18 +28,6 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
'avg_avg_order_value' => 'floatval',
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
*
* @var array
*/
protected $report_columns = array(
'customers_count' => 'COUNT( * ) as customers_count',
'avg_orders_count' => 'AVG( orders_count ) as avg_orders_count',
'avg_total_spend' => 'AVG( total_spend ) as avg_total_spend',
'avg_avg_order_value' => 'AVG( avg_order_value ) as avg_avg_order_value',
);
/**
* Cache identifier.
*
@ -48,10 +36,22 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
protected $cache_key = 'customers_stats';
/**
* Constructor.
* Data store context used to pass to filters.
*
* @var string
*/
public function __construct() {
// This space intentionally left blank (to avoid parent constructor).
protected $context = 'customer_stats';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$this->report_columns = array(
'customers_count' => 'COUNT( * ) as customers_count',
'avg_orders_count' => 'AVG( orders_count ) as avg_orders_count',
'avg_total_spend' => 'AVG( total_spend ) as avg_total_spend',
'avg_avg_order_value' => 'AVG( avg_order_value ) as avg_avg_order_value',
);
}
/**
@ -63,7 +63,7 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$customers_table_name = $wpdb->prefix . self::TABLE_NAME;
$customers_table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -84,6 +84,8 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'customers_count' => 0,
'avg_orders_count' => 0,
@ -93,32 +95,23 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );
// Clear SQL clauses set for parent class queries that are different here.
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', 'SUM( gross_total ) AS total_spend,' );
$this->subquery->add_sql_clause(
'select',
'CASE WHEN COUNT( order_id ) = 0 THEN NULL ELSE COUNT( order_id ) END AS orders_count,'
);
$this->subquery->add_sql_clause(
'select',
'CASE WHEN COUNT( order_id ) = 0 THEN NULL ELSE SUM( gross_total ) / COUNT( order_id ) END AS avg_order_value'
);
$this->clear_sql_clause( array( 'order_by', 'limit' ) );
$this->add_sql_clause( 'select', $selections );
$this->add_sql_clause( 'from', "({$this->subquery->get_query_statement()}) AS tt" );
$report_data = $wpdb->get_results(
"SELECT {$selections} FROM
(
SELECT
(
CASE WHEN COUNT( order_id ) = 0
THEN NULL
ELSE COUNT( order_id )
END
) as orders_count,
SUM( gross_total ) as total_spend,
( SUM( gross_total ) / COUNT( order_id ) ) as avg_order_value
FROM
{$customers_table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
{$customers_table_name}.customer_id
HAVING
1=1
{$sql_query_params['having_clause']}
) as tt",
$this->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.

View File

@ -16,7 +16,7 @@ use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
/**
* Admin\API\Reports\DataStore: Common parent for custom report data stores.
*/
class DataStore {
class DataStore extends SqlQuery {
/**
* Cache group for the reports.
@ -37,7 +37,7 @@ class DataStore {
*
* @var string
*/
const TABLE_NAME = '';
protected static $table_name = '';
/**
* Mapping columns to data type to return correct response types.
@ -46,13 +46,6 @@ class DataStore {
*/
protected $column_types = array();
/**
* SQL columns to select in the db query.
*
* @var array
*/
protected $report_columns = array();
// @todo This does not really belong here, maybe factor out the comparison as separate class?
/**
* Order by property, used in the cmp function.
@ -66,6 +59,65 @@ class DataStore {
* @var string
*/
private $order = '';
/**
* Query limit parameters.
*
* @var array
*/
private $limit_parameters = array();
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'reports';
/**
* Subquery object for query nesting.
*
* @var SqlQuery
*/
protected $subquery;
/**
* Totals query object.
*
* @var SqlQuery
*/
protected $total_query;
/**
* Intervals query object.
*
* @var SqlQuery
*/
protected $interval_query;
/**
* Class constructor.
*/
public function __construct() {
self::set_db_table_name();
$this->assign_report_columns();
}
/**
* Get table name from database class.
*/
public static function get_db_table_name() {
global $wpdb;
return isset( $wpdb->{static::$table_name} ) ? $wpdb->{static::$table_name} : $wpdb->prefix . static::$table_name;
}
/**
* Set table name from database class.
*/
protected static function set_db_table_name() {
global $wpdb;
if ( static::$table_name && ! isset( $wpdb->{static::$table_name} ) ) {
$wpdb->{static::$table_name} = $wpdb->prefix . static::$table_name;
}
}
/**
* Returns string to be used as cache key for the data.
@ -328,16 +380,17 @@ class DataStore {
* If there are less records in the database than time intervals, then we need to remap offset in SQL query
* to fetch correct records.
*
* @param array $intervals_query Array with clauses for the Intervals SQL query.
* @param array $query_args Query arguments.
* @param int $db_interval_count Database interval count.
* @param int $expected_interval_count Expected interval count on the output.
* @param string $table_name Name of the db table relevant for the date constraint.
*/
protected function update_intervals_sql_params( &$intervals_query, &$query_args, $db_interval_count, $expected_interval_count, $table_name ) {
protected function update_intervals_sql_params( &$query_args, $db_interval_count, $expected_interval_count, $table_name ) {
if ( $db_interval_count === $expected_interval_count ) {
return;
}
$params = $this->get_limit_params( $query_args );
$local_tz = new \DateTimeZone( wc_timezone_string() );
if ( 'date' === strtolower( $query_args['orderby'] ) ) {
// page X in request translates to slightly different dates in the db, in case some
@ -347,7 +400,7 @@ class DataStore {
if ( 'asc' === strtolower( $query_args['order'] ) ) {
// ORDER BY date ASC.
$new_start_date = $query_args['after'];
$intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page'];
$intervals_to_skip = ( $query_args['page'] - 1 ) * $params['per_page'];
$latest_end_date = $query_args['before'];
for ( $i = 0; $i < $intervals_to_skip; $i++ ) {
if ( $new_start_date > $latest_end_date ) {
@ -360,7 +413,7 @@ class DataStore {
}
$new_end_date = clone $new_start_date;
for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) {
for ( $i = 0; $i < $params['per_page']; $i++ ) {
if ( $new_end_date > $latest_end_date ) {
break;
}
@ -378,7 +431,7 @@ class DataStore {
} else {
// ORDER BY date DESC.
$new_end_date = $query_args['before'];
$intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page'];
$intervals_to_skip = ( $query_args['page'] - 1 ) * $params['per_page'];
$earliest_start_date = $query_args['after'];
for ( $i = 0; $i < $intervals_to_skip; $i++ ) {
if ( $new_end_date < $earliest_start_date ) {
@ -391,7 +444,7 @@ class DataStore {
}
$new_start_date = clone $new_end_date;
for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) {
for ( $i = 0; $i < $params['per_page']; $i++ ) {
if ( $new_start_date < $earliest_start_date ) {
break;
}
@ -413,21 +466,24 @@ class DataStore {
$query_args['adj_before'] = $new_end_date;
$adj_after = $new_start_date->format( TimeInterval::$sql_datetime_format );
$adj_before = $new_end_date->format( TimeInterval::$sql_datetime_format );
$intervals_query['where_time_clause'] = '';
$intervals_query['where_time_clause'] .= " AND {$table_name}.date_created <= '$adj_before'";
$intervals_query['where_time_clause'] .= " AND {$table_name}.date_created >= '$adj_after'";
$intervals_query['limit'] = 'LIMIT 0,' . $intervals_query['per_page'];
$this->interval_query->clear_sql_clause( array( 'where_time', 'limit' ) );
$this->interval_query->add_sql_clause( 'where_time', "AND {$table_name}.date_created <= '$adj_before'" );
$this->interval_query->add_sql_clause( 'where_time', "AND {$table_name}.date_created >= '$adj_after'" );
$this->clear_sql_clause( 'limit' );
$this->add_sql_clause( 'limit', 'LIMIT 0,' . $params['per_page'] );
} else {
if ( 'asc' === $query_args['order'] ) {
$offset = ( ( $query_args['page'] - 1 ) * $intervals_query['per_page'] ) - ( $expected_interval_count - $db_interval_count );
$offset = ( ( $query_args['page'] - 1 ) * $params['per_page'] ) - ( $expected_interval_count - $db_interval_count );
$offset = $offset < 0 ? 0 : $offset;
$count = $query_args['page'] * $intervals_query['per_page'] - ( $expected_interval_count - $db_interval_count );
$count = $query_args['page'] * $params['per_page'] - ( $expected_interval_count - $db_interval_count );
if ( $count < 0 ) {
$count = 0;
} elseif ( $count > $intervals_query['per_page'] ) {
$count = $intervals_query['per_page'];
} elseif ( $count > $params['per_page'] ) {
$count = $params['per_page'];
}
$intervals_query['limit'] = 'LIMIT ' . $offset . ',' . $count;
$this->clear_sql_clause( 'limit' );
$this->add_sql_clause( 'limit', 'LIMIT ' . $offset . ',' . $count );
}
// Otherwise no change in limit clause.
// @todo - Do this without modifying $query_args?
@ -592,14 +648,12 @@ class DataStore {
*
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint.
* @return array
*/
protected function get_time_period_sql_params( $query_args, $table_name ) {
$sql_query = array(
'from_clause' => '',
'where_time_clause' => '',
'where_clause' => '',
);
$this->clear_sql_clause( array( 'from', 'where_time', 'where' ) );
if ( isset( $this->subquery ) ) {
$this->subquery->clear_sql_clause( 'where_time' );
}
if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) {
if ( is_a( $query_args['before'], 'WC_DateTime' ) ) {
@ -607,8 +661,11 @@ class DataStore {
} else {
$datetime_str = $query_args['before']->format( TimeInterval::$sql_datetime_format );
}
$sql_query['where_time_clause'] .= " AND {$table_name}.date_created <= '$datetime_str'";
if ( isset( $this->subquery ) ) {
$this->subquery->add_sql_clause( 'where_time', "AND {$table_name}.date_created <= '$datetime_str'" );
} else {
$this->add_sql_clause( 'where_time', "AND {$table_name}.date_created <= '$datetime_str'" );
}
}
if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) {
@ -617,10 +674,12 @@ class DataStore {
} else {
$datetime_str = $query_args['after']->format( TimeInterval::$sql_datetime_format );
}
$sql_query['where_time_clause'] .= " AND {$table_name}.date_created >= '$datetime_str'";
if ( isset( $this->subquery ) ) {
$this->subquery->add_sql_clause( 'where_time', "AND {$table_name}.date_created >= '$datetime_str'" );
} else {
$this->add_sql_clause( 'where_time', "AND {$table_name}.date_created >= '$datetime_str'" );
}
}
return $sql_query;
}
/**
@ -630,18 +689,32 @@ class DataStore {
* @return array
*/
protected function get_limit_sql_params( $query_args ) {
$sql_query['per_page'] = get_option( 'posts_per_page' );
$params = $this->get_limit_params( $query_args );
$this->clear_sql_clause( 'limit' );
$this->add_sql_clause( 'limit', "LIMIT {$params['offset']}, {$params['per_page']}" );
return $params;
}
/**
* Fills LIMIT parameters of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_limit_params( $query_args = array() ) {
if ( isset( $query_args['per_page'] ) && is_numeric( $query_args['per_page'] ) ) {
$sql_query['per_page'] = (int) $query_args['per_page'];
$this->limit_parameters['per_page'] = (int) $query_args['per_page'];
} else {
$this->limit_parameters['per_page'] = get_option( 'posts_per_page' );
}
$sql_query['offset'] = 0;
$this->limit_parameters['offset'] = 0;
if ( isset( $query_args['page'] ) ) {
$sql_query['offset'] = ( (int) $query_args['page'] - 1 ) * $sql_query['per_page'];
$this->limit_parameters['offset'] = ( (int) $query_args['page'] - 1 ) * $this->limit_parameters['per_page'];
}
$sql_query['limit'] = "LIMIT {$sql_query['offset']}, {$sql_query['per_page']}";
return $sql_query;
return $this->limit_parameters;
}
/**
@ -703,21 +776,17 @@ class DataStore {
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_order_by_sql_params( $query_args ) {
$sql_query['order_by_clause'] = '';
if ( isset( $query_args['orderby'] ) ) {
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
}
if ( isset( $query_args['order'] ) ) {
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
$order_by_clause = $this->normalize_order_by( $query_args['orderby'] );
} else {
$sql_query['order_by_clause'] .= ' DESC';
$order_by_clause = '';
}
return $sql_query;
$this->clear_sql_clause( 'order_by' );
$this->add_sql_clause( 'order_by', $order_by_clause );
$this->add_orderby_order_clause( $query_args, $this );
}
/**
@ -725,27 +794,17 @@ class DataStore {
*
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint.
* @return array
*/
protected function get_intervals_sql_params( $query_args, $table_name ) {
$intervals_query = array(
'from_clause' => '',
'where_time_clause' => '',
'where_clause' => '',
);
$this->clear_sql_clause( array( 'from', 'where_time', 'where' ) );
$intervals_query = array_merge( $intervals_query, $this->get_time_period_sql_params( $query_args, $table_name ) );
$this->get_time_period_sql_params( $query_args, $table_name );
if ( isset( $query_args['interval'] ) && '' !== $query_args['interval'] ) {
$interval = $query_args['interval'];
$intervals_query['select_clause'] = TimeInterval::db_datetime_format( $interval, $table_name );
$this->clear_sql_clause( 'select' );
$this->add_sql_clause( 'select', TimeInterval::db_datetime_format( $interval, $table_name ) );
}
$intervals_query = array_merge( $intervals_query, $this->get_limit_sql_params( $query_args ) );
$intervals_query = array_merge( $intervals_query, $this->get_order_by_sql_params( $query_args ) );
return $intervals_query;
}
/**
@ -815,6 +874,35 @@ class DataStore {
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.
*
* @return string
*/
protected function get_object_where_filter( $select_table, $select_field, $filter_table, $filter_field, $compare, $id_list ) {
global $wpdb;
if ( empty( $id_list ) ) {
return '';
}
$lookup_name = isset( $wpdb->$filter_table ) ? $wpdb->$filter_table : $wpdb->prefix . $filter_table;
return " {$select_table}.{$select_field} {$compare} (
SELECT
DISTINCT {$filter_table}.{$select_field}
FROM
{$filter_table}
WHERE
{$filter_table}.{$filter_field} IN ({$id_list})
)";
}
/**
* Returns an array of ids of allowed products, based on query arguments from the user.
*
@ -864,15 +952,11 @@ class DataStore {
* @return string
*/
protected function get_included_variations( $query_args ) {
$included_variations = array();
$operator = $this->get_match_operator( $query_args );
if ( isset( $query_args['variations'] ) && is_array( $query_args['variations'] ) && count( $query_args['variations'] ) > 0 ) {
$included_variations = array_filter( array_map( 'intval', $query_args['variations'] ) );
$query_args['variations'] = array_filter( array_map( 'intval', $query_args['variations'] ) );
}
$included_variations_str = implode( ',', $included_variations );
return $included_variations_str;
return $this->get_filtered_ids( $query_args, 'variations' );
}
/**
@ -882,27 +966,28 @@ class DataStore {
* @return string
*/
protected function get_excluded_products( $query_args ) {
$excluded_products_str = '';
return $this->get_filtered_ids( $query_args, 'product_excludes' );
}
if ( isset( $query_args['product_excludes'] ) && is_array( $query_args['product_excludes'] ) && count( $query_args['product_excludes'] ) > 0 ) {
$excluded_products_str = implode( ',', $query_args['product_excludes'] );
}
return $excluded_products_str;
/**
* Returns comma separated ids of included categories, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_categories( $query_args ) {
return $this->get_filtered_ids( $query_args, 'categories' );
}
/**
* Returns comma separated ids of included coupons, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @param array $query_args Parameters supplied by the user.
* @param string $field Field name in the parameter list.
* @return string
*/
protected function get_included_coupons( $query_args ) {
$included_coupons_str = '';
if ( isset( $query_args['coupon_includes'] ) && is_array( $query_args['coupon_includes'] ) && count( $query_args['coupon_includes'] ) > 0 ) {
$included_coupons_str = implode( ',', $query_args['coupon_includes'] );
}
return $included_coupons_str;
protected function get_included_coupons( $query_args, $field = 'coupon_includes' ) {
return $this->get_filtered_ids( $query_args, $field );
}
/**
@ -912,12 +997,7 @@ class DataStore {
* @return string
*/
protected function get_excluded_coupons( $query_args ) {
$excluded_coupons_str = '';
if ( isset( $query_args['coupon_excludes'] ) && is_array( $query_args['coupon_excludes'] ) && count( $query_args['coupon_excludes'] ) > 0 ) {
$excluded_coupons_str = implode( ',', $query_args['coupon_excludes'] );
}
return $excluded_coupons_str;
return $this->get_filtered_ids( $query_args, 'coupon_excludes' );
}
/**
@ -927,12 +1007,7 @@ class DataStore {
* @return string
*/
protected function get_included_orders( $query_args ) {
$included_orders_str = '';
if ( isset( $query_args['order_includes'] ) && is_array( $query_args['order_includes'] ) && count( $query_args['order_includes'] ) > 0 ) {
$included_orders_str = implode( ',', $query_args['order_includes'] );
}
return $included_orders_str;
return $this->get_filtered_ids( $query_args, 'order_includes' );
}
/**
@ -942,12 +1017,7 @@ class DataStore {
* @return string
*/
protected function get_excluded_orders( $query_args ) {
$excluded_orders_str = '';
if ( isset( $query_args['order_excludes'] ) && is_array( $query_args['order_excludes'] ) && count( $query_args['order_excludes'] ) > 0 ) {
$excluded_orders_str = implode( ',', $query_args['order_excludes'] );
}
return $excluded_orders_str;
return $this->get_filtered_ids( $query_args, 'order_excludes' );
}
/**
@ -957,12 +1027,7 @@ class DataStore {
* @return string
*/
protected function get_included_users( $query_args ) {
$included_users_str = '';
if ( isset( $query_args['user_includes'] ) && is_array( $query_args['user_includes'] ) && count( $query_args['user_includes'] ) > 0 ) {
$included_users_str = implode( ',', $query_args['user_includes'] );
}
return $included_users_str;
return $this->get_filtered_ids( $query_args, 'user_includes' );
}
/**
@ -972,12 +1037,7 @@ class DataStore {
* @return string
*/
protected function get_excluded_users( $query_args ) {
$excluded_users_str = '';
if ( isset( $query_args['user_excludes'] ) && is_array( $query_args['user_excludes'] ) && count( $query_args['user_excludes'] ) > 0 ) {
$excluded_users_str = implode( ',', $query_args['user_excludes'] );
}
return $excluded_users_str;
return $this->get_filtered_ids( $query_args, 'user_excludes' );
}
/**
@ -1016,6 +1076,56 @@ class DataStore {
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 ) {
global $wpdb;
$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 ) {
$order_by_clause = '';
$sql_query->clear_sql_clause( array( 'order_by' ) );
if ( isset( $query_args['orderby'] ) ) {
$order_by_clause = $this->normalize_order_by( $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.
return $order_by_clause;
}
/**
* 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', $query_args['order'] );
} else {
$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.
*
@ -1057,4 +1167,39 @@ class DataStore {
}
return $operator;
}
/**
* Returns filtered comma separated ids, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @param string $field Query field to filter.
* @param string $separator Field separator.
* @return string
*/
protected function get_filtered_ids( $query_args, $field, $separator = ',' ) {
$ids_str = '';
$ids = isset( $query_args[ $field ] ) && is_array( $query_args[ $field ] ) ? $query_args[ $field ] : array();
/**
* Filter the IDs before retrieving report data.
*
* Allows filtering of the objects included or excluded from reports.
*
* @param array $ids List of object Ids.
* @param array $query_args The original arguments for the request.
* @param string $field The object type.
* @param string $context The data store context.
*/
$ids = apply_filters( 'wc_admin_reports_ ' . $field, $ids, $query_args, $field, $this->context );
if ( ! empty( $ids ) ) {
$ids_str = implode( $separator, $ids );
}
return $ids_str;
}
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Downloads\DataStore.
@ -23,7 +24,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_download_log';
protected static $table_name = 'wc_download_log';
/**
* Cache identifier.
@ -50,139 +51,136 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array(
'id' => 'download_log_id as id',
'date' => 'timestamp as date_gmt',
'download_id' => 'product_permissions.download_id',
'product_id' => 'product_permissions.product_id',
'order_id' => 'product_permissions.order_id',
'user_id' => 'product_permissions.user_id',
'ip_address' => 'user_ip_address as ip_address',
);
protected $context = 'downloads';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
protected function assign_report_columns() {
$this->report_columns = array(
'id' => 'download_log_id as id',
'date' => 'timestamp as date_gmt',
'download_id' => 'product_permissions.download_id',
'product_id' => 'product_permissions.product_id',
'order_id' => 'product_permissions.order_id',
'user_id' => 'product_permissions.user_id',
'ip_address' => 'user_ip_address as ip_address',
);
}
/**
* Updates the database query with parameters used for downloads report.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$lookup_table = $wpdb->prefix . self::TABLE_NAME;
$operator = $this->get_match_operator( $query_args );
$where_filters = array();
$lookup_table = self::get_db_table_name();
$permission_table = $wpdb->prefix . 'woocommerce_downloadable_product_permissions';
$operator = $this->get_match_operator( $query_args );
$where_filters = array();
$join = "JOIN {$permission_table} as product_permissions ON {$lookup_table}.permission_id = product_permissions.permission_id";
$sql_query_params = $this->get_time_period_sql_params( $query_args, $lookup_table );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$included_products = $this->get_included_products( $query_args );
$excluded_products = $this->get_excluded_products( $query_args );
if ( $included_products ) {
$where_filters[] = " {$lookup_table}.permission_id IN (
SELECT
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
FROM
{$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE
{$wpdb->prefix}woocommerce_downloadable_product_permissions.product_id IN ({$included_products})
)";
$where_time = $this->get_time_period_sql_params( $query_args, $lookup_table );
if ( $where_time ) {
if ( isset( $this->subquery ) ) {
$this->subquery->add_sql_clause( 'where_time', $where_time );
} else {
$this->interval_query->add_sql_clause( 'where_time', $where_time );
}
}
$this->get_limit_sql_params( $query_args );
if ( $excluded_products ) {
$where_filters[] = " {$lookup_table}.permission_id NOT IN (
SELECT
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
FROM
{$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE
{$wpdb->prefix}woocommerce_downloadable_product_permissions.product_id IN ({$excluded_products})
)";
}
$included_orders = $this->get_included_orders( $query_args );
$excluded_orders = $this->get_excluded_orders( $query_args );
if ( $included_orders ) {
$where_filters[] = " {$lookup_table}.permission_id IN (
SELECT
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
FROM
{$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE
{$wpdb->prefix}woocommerce_downloadable_product_permissions.order_id IN ({$included_orders})
)";
}
if ( $excluded_orders ) {
$where_filters[] = " {$lookup_table}.permission_id NOT IN (
SELECT
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
FROM
{$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE
{$wpdb->prefix}woocommerce_downloadable_product_permissions.order_id IN ({$excluded_orders})
)";
}
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'product_id',
'IN',
$this->get_included_products( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'product_id',
'NOT IN',
$this->get_excluded_products( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'order_id',
'IN',
$this->get_included_orders( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'order_id',
'NOT IN',
$this->get_excluded_orders( $query_args )
);
$customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
$customer_lookup = "SELECT {$customer_lookup_table}.user_id FROM {$customer_lookup_table} WHERE {$customer_lookup_table}.customer_id IN (%s)";
$included_customers = $this->get_included_customers( $query_args );
$excluded_customers = $this->get_excluded_customers( $query_args );
if ( $included_customers ) {
$where_filters[] = " {$lookup_table}.permission_id IN (
SELECT
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
FROM
{$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE
{$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN (
SELECT {$customer_lookup_table}.user_id FROM {$customer_lookup_table} WHERE {$customer_lookup_table}.customer_id IN ({$included_customers})
)
)";
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'user_id',
'IN',
sprintf( $customer_lookup, $included_customers )
);
}
if ( $excluded_customers ) {
$where_filters[] = " {$lookup_table}.permission_id NOT IN (
SELECT
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
FROM
{$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE
{$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN (
SELECT {$customer_lookup_table}.user_id FROM {$customer_lookup_table} WHERE {$customer_lookup_table}.customer_id IN ({$excluded_customers})
)
)";
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'user_id',
'NOT IN',
sprintf( $customer_lookup, $excluded_customers )
);
}
$included_ip_addresses = $this->get_included_ip_addresses( $query_args );
$excluded_ip_addresses = $this->get_excluded_ip_addresses( $query_args );
if ( $included_ip_addresses ) {
$where_filters[] = " {$lookup_table}.user_ip_address IN ('{$included_ip_addresses}')";
$where_filters[] = "{$lookup_table}.user_ip_address IN ('{$included_ip_addresses}')";
}
if ( $excluded_ip_addresses ) {
$where_filters[] = " {$lookup_table}.user_ip_address NOT IN ('{$excluded_ip_addresses}')";
$where_filters[] = "{$lookup_table}.user_ip_address NOT IN ('{$excluded_ip_addresses}')";
}
$where_filters = array_filter( $where_filters );
$where_subclause = implode( " $operator ", $where_filters );
if ( $where_subclause ) {
$sql_query_params['where_clause'] .= " AND ( $where_subclause )";
if ( isset( $this->subquery ) ) {
$this->subquery->add_sql_clause( 'where', "AND ( $where_subclause )" );
} else {
$this->interval_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
}
}
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}woocommerce_downloadable_product_permissions as product_permissions ON {$lookup_table}.permission_id = product_permissions.permission_id";
$sql_query_params = $this->get_order_by( $query_args, $sql_query_params );
return $sql_query_params;
if ( isset( $this->subquery ) ) {
$this->subquery->add_sql_clause( 'join', $join );
} else {
$this->interval_query->add_sql_clause( 'join', $join );
}
$this->get_order_by( $query_args );
}
/**
@ -192,16 +190,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* @return string
*/
protected function get_included_ip_addresses( $query_args ) {
$included_ips_str = '';
if ( isset( $query_args['ip_address_includes'] ) && is_array( $query_args['ip_address_includes'] ) && count( $query_args['ip_address_includes'] ) > 0 ) {
$ip_includes = array();
foreach ( $query_args['ip_address_includes'] as $ip ) {
$ip_includes[] = esc_sql( $ip );
}
$included_ips_str = implode( "','", $ip_includes );
$query_args['ip_address_includes'] = array_map( 'esc_sql', $query_args['ip_address_includes'] );
}
return $included_ips_str;
return self::get_filtered_ids( $query_args, 'ip_address_includes', "','" );
}
/**
@ -211,16 +203,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* @return string
*/
protected function get_excluded_ip_addresses( $query_args ) {
$excluded_ips_str = '';
if ( isset( $query_args['ip_address_excludes'] ) && is_array( $query_args['ip_address_excludes'] ) && count( $query_args['ip_address_excludes'] ) > 0 ) {
$ip_excludes = array();
foreach ( $query_args['ip_address_excludes'] as $ip ) {
$ip_excludes[] = esc_sql( $ip );
}
$excluded_ips_str = implode( ',', $ip_excludes );
$query_args['ip_address_excludes'] = array_map( 'esc_sql', $query_args['ip_address_excludes'] );
}
return $excluded_ips_str;
return self::get_filtered_ids( $query_args, 'ip_address_excludes', "','" );
}
/**
@ -230,12 +216,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* @return string
*/
protected function get_included_customers( $query_args ) {
$included_customers_str = '';
if ( isset( $query_args['customer_includes'] ) && is_array( $query_args['customer_includes'] ) && count( $query_args['customer_includes'] ) > 0 ) {
$included_customers_str = implode( ',', $query_args['customer_includes'] );
}
return $included_customers_str;
return self::get_filtered_ids( $query_args, 'customer_includes' );
}
/**
@ -245,67 +226,51 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* @return string
*/
protected function get_excluded_customers( $query_args ) {
$excluded_customer_str = '';
if ( isset( $query_args['customer_excludes'] ) && is_array( $query_args['customer_excludes'] ) && count( $query_args['customer_excludes'] ) > 0 ) {
$excluded_customer_str = implode( ',', $query_args['customer_excludes'] );
}
return $excluded_customer_str;
return self::get_filtered_ids( $query_args, 'customer_excludes' );
}
/**
* Fills WHERE clause of SQL request with date-related constraints.
* Gets WHERE time clause of SQL request with date-related constraints.
*
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint.
* @return array
* @return string
*/
protected function get_time_period_sql_params( $query_args, $table_name ) {
$sql_query = array(
'from_clause' => '',
'where_time_clause' => '',
'where_clause' => '',
);
$where_time = '';
if ( $query_args['before'] ) {
$datetime_str = $query_args['before']->format( TimeInterval::$sql_datetime_format );
$sql_query['where_time_clause'] .= " AND {$table_name}.timestamp <= '$datetime_str'";
$datetime_str = $query_args['before']->format( TimeInterval::$sql_datetime_format );
$where_time .= " AND {$table_name}.timestamp <= '$datetime_str'";
}
if ( $query_args['after'] ) {
$datetime_str = $query_args['after']->format( TimeInterval::$sql_datetime_format );
$sql_query['where_time_clause'] .= " AND {$table_name}.timestamp >= '$datetime_str'";
$datetime_str = $query_args['after']->format( TimeInterval::$sql_datetime_format );
$where_time .= " AND {$table_name}.timestamp >= '$datetime_str'";
}
return $sql_query;
return $where_time;
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param array $sql_query Current SQL query array.
* @return array
*/
protected function get_order_by( $query_args, $sql_query ) {
protected function get_order_by( $query_args ) {
global $wpdb;
$sql_query['order_by_clause'] = '';
$this->clear_sql_clause( 'order_by' );
$order_by = '';
if ( isset( $query_args['orderby'] ) ) {
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
$order_by = $this->normalize_order_by( $query_args['orderby'] );
$this->add_sql_clause( 'order_by', $order_by );
}
if ( false !== strpos( $sql_query['order_by_clause'], '_products' ) ) {
$sql_query['from_clause'] .= " JOIN {$wpdb->prefix}posts AS _products ON product_permissions.product_id = _products.ID";
if ( false !== strpos( $order_by, '_products' ) ) {
$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->posts} AS _products ON product_permissions.product_id = _products.ID" );
}
if ( isset( $query_args['order'] ) ) {
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
} else {
$sql_query['order_by_clause'] .= ' DESC';
}
return $sql_query;
$this->add_orderby_order_clause( $query_args, $this );
}
/**
@ -317,7 +282,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -340,6 +305,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -352,41 +319,23 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT
{$table_name}.download_log_id
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
{$table_name}.download_log_id
) AS tt"
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
$params = $this->get_limit_params( $query_args );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$download_data = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
{$table_name}.download_log_id
ORDER BY
{$sql_query_params['order_by_clause']}
{$sql_query_params['limit']}
",
$this->subquery->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -427,4 +376,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return $order_by;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$table_name = self::get_db_table_name();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'from', $table_name );
$this->subquery->add_sql_clause( 'select', "{$table_name}.download_log_id" );
$this->subquery->add_sql_clause( 'group_by', "{$table_name}.download_log_id" );
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore as DownloadsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Downloads\Stats\DataStore.
@ -27,15 +28,6 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
'download_count' => 'intval',
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
*
* @var array
*/
protected $report_columns = array(
'download_count' => 'COUNT(DISTINCT download_log_id) as download_count',
);
/**
* Cache identifier.
*
@ -44,12 +36,20 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
protected $cache_key = 'downloads_stats';
/**
* Constructor
* Data store context used to pass to filters.
*
* @var string
*/
public function __construct() {
global $wpdb;
}
protected $context = 'download_stats';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$this->report_columns = array(
'download_count' => 'COUNT(DISTINCT download_log_id) as download_count',
);
}
/**
* Returns the report data based on parameters supplied by the user.
@ -60,7 +60,7 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -84,53 +84,35 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );
$totals_query = array_merge( array(), $this->get_time_period_sql_params( $query_args, $table_name ) );
$intervals_query = array_merge( array(), $this->get_intervals_sql_params( $query_args, $table_name ) );
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$this->get_sql_query_params( $query_args );
$this->get_time_period_sql_params( $query_args, $table_name );
$this->get_intervals_sql_params( $query_args, $table_name );
$totals_query['where_clause'] .= $sql_query_params['where_clause'];
$totals_query['from_clause'] .= $sql_query_params['from_clause'];
$intervals_query['where_clause'] .= $sql_query_params['where_clause'];
$intervals_query['from_clause'] .= $sql_query_params['from_clause'];
$intervals_query['select_clause'] = str_replace( 'date_created', 'timestamp', $intervals_query['select_clause'] );
$intervals_query['where_time_clause'] = str_replace( 'date_created', 'timestamp', $intervals_query['where_time_clause'] );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
$this->interval_query->str_replace_clause( 'select', 'date_created', 'timestamp' );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$db_intervals = $wpdb->get_col(
"SELECT
{$intervals_query['select_clause']} AS time_interval
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval"
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, , unprepared SQL ok.
$db_records_count = count( $db_intervals );
$params = $this->get_limit_params( $query_args );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] );
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return array();
}
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_records_count, $expected_interval_count, $table_name );
$intervals_query['where_time_clause'] = str_replace( 'date_created', 'timestamp', $intervals_query['where_time_clause'] );
$this->update_intervals_sql_params( $query_args, $db_records_count, $expected_interval_count, $table_name );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'where', $this->interval_query->get_sql_clause( 'where' ) );
$totals = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}",
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -138,27 +120,15 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
return new \WP_Error( 'woocommerce_reports_downloads_stats_result_failed', __( 'Sorry, fetching downloads data failed.', 'woocommerce-admin' ) );
}
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'select', ', MAX(timestamp) AS datetime_anchor' );
if ( '' !== $selections ) {
$selections = ', ' . $selections;
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$intervals = $wpdb->get_results(
"SELECT
MAX(timestamp) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval
{$selections}
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval
ORDER BY
{$intervals_query['order_by_clause']}
{$intervals_query['limit']}",
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -175,10 +145,10 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
'page_no' => (int) $query_args['page'],
);
if ( $this->intervals_missing( $expected_interval_count, $db_records_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
if ( $this->intervals_missing( $expected_interval_count, $db_records_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}
@ -203,4 +173,18 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
return $order_by;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}

View File

@ -11,6 +11,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Orders\DataStore.
@ -22,7 +23,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_order_stats';
protected static $table_name = 'wc_order_stats';
/**
* Cache identifier.
@ -50,18 +51,17 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array();
protected $context = 'orders';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
// Avoid ambigious columns in SQL query.
$this->report_columns = array(
'order_id' => "{$table_name}.order_id",
@ -81,22 +81,23 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* Updates the database query with parameters used for orders report: coupons and products filters.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$order_stats_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$operator = $this->get_match_operator( $query_args );
$where_subquery = array();
$order_stats_lookup_table = self::get_db_table_name();
$order_coupon_lookup_table = $wpdb->prefix . 'wc_order_coupon_lookup';
$order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup';
$operator = $this->get_match_operator( $query_args );
$where_subquery = array();
$sql_query_params = $this->get_time_period_sql_params( $query_args, $order_stats_lookup_table );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
$this->get_time_period_sql_params( $query_args, $order_stats_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->get_order_by_sql_params( $query_args );
$status_subquery = $this->get_status_subquery( $query_args );
if ( $status_subquery ) {
if ( empty( $query_args['status_is'] ) && empty( $query_args['status_is_not'] ) ) {
$sql_query_params['where_clause'] .= " AND {$status_subquery}";
$this->subquery->add_sql_clause( 'where', "AND {$status_subquery}" );
} else {
$where_subquery[] = $status_subquery;
}
@ -117,17 +118,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$where_subquery[] = "{$order_stats_lookup_table}.returning_customer = ${returning_customer}";
}
$refund_subquery = $this->get_refund_subquery( $query_args );
$sql_query_params['from_clause'] .= $refund_subquery['from_clause'] ? $refund_subquery['from_clause'] : '';
$refund_subquery = $this->get_refund_subquery( $query_args );
$this->subquery->add_sql_clause( 'from', $refund_subquery['from_clause'] );
if ( $refund_subquery['where_clause'] ) {
$where_subquery[] = $refund_subquery['where_clause'];
}
$included_coupons = $this->get_included_coupons( $query_args );
$excluded_coupons = $this->get_excluded_coupons( $query_args );
$order_coupon_lookup_table = $wpdb->prefix . 'wc_order_coupon_lookup';
$included_coupons = $this->get_included_coupons( $query_args );
$excluded_coupons = $this->get_excluded_coupons( $query_args );
if ( $included_coupons || $excluded_coupons ) {
$sql_query_params['from_clause'] .= " JOIN {$order_coupon_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_coupon_lookup_table}.order_id";
$this->subquery->add_sql_clause( 'join', "JOIN {$order_coupon_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_coupon_lookup_table}.order_id" );
}
if ( $included_coupons ) {
$where_subquery[] = "{$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})";
@ -136,11 +136,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$where_subquery[] = "{$order_coupon_lookup_table}.coupon_id NOT IN ({$excluded_coupons})";
}
$included_products = $this->get_included_products( $query_args );
$excluded_products = $this->get_excluded_products( $query_args );
$order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup';
$included_products = $this->get_included_products( $query_args );
$excluded_products = $this->get_excluded_products( $query_args );
if ( $included_products || $excluded_products ) {
$sql_query_params['from_clause'] .= " JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id";
$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );
}
if ( $included_products ) {
$where_subquery[] = "{$order_product_lookup_table}.product_id IN ({$included_products})";
@ -150,10 +149,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
if ( 0 < count( $where_subquery ) ) {
$sql_query_params['where_clause'] .= ' AND (' . implode( " {$operator} ", $where_subquery ) . ')';
$this->subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $where_subquery ) . ')' );
}
return $sql_query_params;
}
/**
@ -165,7 +162,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -198,6 +195,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -205,26 +204,19 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$this->get_sql_query_params( $query_args );
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT
{$table_name}.order_id
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
) AS tt"
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( 0 === $sql_query_params['per_page'] ) {
if ( 0 === $params['per_page'] ) {
$total_pages = 0;
} else {
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
}
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
$data = (object) array(
@ -236,20 +228,12 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$orders_data = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
ORDER BY
{$sql_query_params['order_by_clause']}
{$sql_query_params['limit']}
",
$this->subquery->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -271,7 +255,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
@ -368,8 +351,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$products = $wpdb->get_results(
"SELECT order_id, ID as product_id, post_title as product_name, product_qty as product_quantity
FROM {$wpdb->prefix}posts
JOIN {$order_product_lookup_table} ON {$order_product_lookup_table}.product_id = {$wpdb->prefix}posts.ID
FROM {$wpdb->posts}
JOIN {$order_product_lookup_table} ON {$order_product_lookup_table}.product_id = {$wpdb->posts}.ID
WHERE
order_id IN ({$included_order_ids})
",
@ -423,8 +406,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$coupons = $wpdb->get_results(
"SELECT order_id, coupon_id, post_title as coupon_code
FROM {$wpdb->prefix}posts
JOIN {$order_coupon_lookup_table} ON {$order_coupon_lookup_table}.coupon_id = {$wpdb->prefix}posts.ID
FROM {$wpdb->posts}
JOIN {$order_coupon_lookup_table} ON {$order_coupon_lookup_table}.coupon_id = {$wpdb->posts}.ID
WHERE
order_id IN ({$included_order_ids})
",
@ -433,4 +416,14 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return $coupons;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', self::get_db_table_name() . '.order_id' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
@ -24,7 +25,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_order_stats';
protected static $table_name = 'wc_order_stats';
/**
* Cron event name.
@ -62,18 +63,17 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* SQL definition for each column.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array();
protected $context = 'order_stats';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
// Avoid ambigious columns in SQL query.
$this->report_columns = array(
'orders_count' => "SUM( CASE WHEN {$table_name}.parent_id = 0 THEN 1 ELSE 0 END ) as orders_count",
@ -104,81 +104,63 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* Updates the totals and intervals database queries with parameters used for Orders report: categories, coupons and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @param array $totals_query Array of options for totals db query.
* @param array $intervals_query Array of options for intervals db query.
*/
protected function orders_stats_sql_filter( $query_args, &$totals_query, &$intervals_query ) {
protected function orders_stats_sql_filter( $query_args ) {
// @todo Performance of all of this?
global $wpdb;
$from_clause = '';
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
$orders_stats_table = self::get_db_table_name();
$product_lookup = $wpdb->prefix . 'wc_order_product_lookup';
$coupon_lookup = $wpdb->prefix . 'wc_order_coupon_lookup';
$operator = $this->get_match_operator( $query_args );
$where_filters = array();
// @todo Maybe move the sql inside the get_included/excluded functions?
// Products filters.
$included_products = $this->get_included_products( $query_args );
$excluded_products = $this->get_excluded_products( $query_args );
if ( $included_products ) {
$where_filters[] = " {$orders_stats_table}.order_id IN (
SELECT
DISTINCT {$wpdb->prefix}wc_order_product_lookup.order_id
FROM
{$wpdb->prefix}wc_order_product_lookup
WHERE
{$wpdb->prefix}wc_order_product_lookup.product_id IN ({$included_products})
)";
}
if ( $excluded_products ) {
$where_filters[] = " {$orders_stats_table}.order_id NOT IN (
SELECT
DISTINCT {$wpdb->prefix}wc_order_product_lookup.order_id
FROM
{$wpdb->prefix}wc_order_product_lookup
WHERE
{$wpdb->prefix}wc_order_product_lookup.product_id IN ({$excluded_products})
)";
}
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$product_lookup,
'product_id',
'IN',
$this->get_included_products( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$product_lookup,
'product_id',
'NOT IN',
$this->get_excluded_products( $query_args )
);
// Coupons filters.
$included_coupons = $this->get_included_coupons( $query_args );
$excluded_coupons = $this->get_excluded_coupons( $query_args );
if ( $included_coupons ) {
$where_filters[] = " {$orders_stats_table}.order_id IN (
SELECT
DISTINCT {$wpdb->prefix}wc_order_coupon_lookup.order_id
FROM
{$wpdb->prefix}wc_order_coupon_lookup
WHERE
{$wpdb->prefix}wc_order_coupon_lookup.coupon_id IN ({$included_coupons})
)";
}
if ( $excluded_coupons ) {
$where_filters[] = " {$orders_stats_table}.order_id NOT IN (
SELECT
DISTINCT {$wpdb->prefix}wc_order_coupon_lookup.order_id
FROM
{$wpdb->prefix}wc_order_coupon_lookup
WHERE
{$wpdb->prefix}wc_order_coupon_lookup.coupon_id IN ({$excluded_coupons})
)";
}
$customer_filter = $this->get_customer_subquery( $query_args );
if ( $customer_filter ) {
$where_filters[] = $customer_filter;
}
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$coupon_lookup,
'coupon_id',
'IN',
$this->get_included_coupons( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$coupon_lookup,
'coupon_id',
'NOT IN',
$this->get_excluded_coupons( $query_args )
);
$where_filters[] = $this->get_customer_subquery( $query_args );
$refund_subquery = $this->get_refund_subquery( $query_args );
$from_clause .= $refund_subquery['from_clause'];
if ( $refund_subquery['where_clause'] ) {
$where_filters[] = $refund_subquery['where_clause'];
$from_clause .= $refund_subquery['from_clause'];
}
$where_filters = array_filter( $where_filters );
$where_subclause = implode( " $operator ", $where_filters );
// Append status filter after to avoid matching ANY on default statuses.
@ -192,10 +174,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
// To avoid requesting the subqueries twice, the result is applied to all queries passed to the method.
if ( $where_subclause ) {
$totals_query['where_clause'] .= " AND ( $where_subclause )";
$totals_query['from_clause'] .= $from_clause;
$intervals_query['where_clause'] .= " AND ( $where_subclause )";
$intervals_query['from_clause'] .= $from_clause;
$this->total_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
$this->total_query->add_sql_clause( 'join', $from_clause );
$this->interval_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
$this->interval_query->add_sql_clause( 'join', $from_clause );
}
}
@ -208,7 +190,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc).
$defaults = array(
@ -243,6 +225,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'totals' => (object) array(),
'intervals' => (object) array(),
@ -251,10 +235,13 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$totals_query = $this->get_time_period_sql_params( $query_args, $table_name );
$intervals_query = $this->get_intervals_sql_params( $query_args, $table_name );
$coupon_join = "LEFT JOIN (
$selections = $this->selected_columns( $query_args );
$this->get_time_period_sql_params( $query_args, $table_name );
$this->get_intervals_sql_params( $query_args, $table_name );
$this->get_order_by_sql_params( $query_args );
$where_time = $this->get_sql_clause( 'where_time' );
$params = $this->get_limit_sql_params( $query_args );
$coupon_join = "LEFT JOIN (
SELECT
order_id,
SUM(discount_amount) AS discount_amount,
@ -267,25 +254,32 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
ON order_coupon_lookup.order_id = {$wpdb->prefix}wc_order_stats.order_id";
// Additional filtering for Orders report.
$this->orders_stats_sql_filter( $query_args, $totals_query, $intervals_query );
$this->orders_stats_sql_filter( $query_args );
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'left_join', $coupon_join );
$this->total_query->add_sql_clause( 'where_time', $where_time );
$totals = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$totals_query['from_clause']}
{$coupon_join}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}",
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce-admin' ) );
}
// @todo remove these assignements when refactoring segmenter classes to use query objects.
$totals_query = array(
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
'where_time_clause' => $where_time,
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
);
$intervals_query = array(
'select_clause' => $this->get_sql_clause( 'select' ),
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
'where_time_clause' => $where_time,
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$unique_products = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
$totals[0]['products'] = $unique_products;
$segmenter = new Segmenter( $query_args, $this->report_columns );
@ -294,53 +288,30 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$totals = (object) $this->cast_numbers( $totals[0] );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
$this->interval_query->add_sql_clause( 'left_join', $coupon_join );
$this->interval_query->add_sql_clause( 'where_time', $where_time );
$db_intervals = $wpdb->get_col(
"SELECT
{$intervals_query['select_clause']} AS time_interval
FROM
{$table_name}
{$intervals_query['from_clause']}
{$coupon_join}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval"
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, , unprepared SQL ok.
$db_interval_count = count( $db_intervals );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] );
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$selections = ', ' . $selections;
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$intervals = $wpdb->get_results(
"SELECT
MAX({$table_name}.date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval
{$selections}
FROM
{$table_name}
{$intervals_query['from_clause']}
{$coupon_join}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval
ORDER BY
{$intervals_query['order_by_clause']}
{$intervals_query['limit']}",
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -361,10 +332,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'page_no' => (int) $query_args['page'],
);
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}
@ -388,8 +359,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_unique_product_count( $from_clause, $where_time_clause, $where_clause ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
return $wpdb->get_var(
"SELECT
COUNT( DISTINCT {$wpdb->prefix}wc_order_product_lookup.product_id )
@ -414,7 +384,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_unique_coupon_count( $from_clause, $where_time_clause, $where_clause ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
return $wpdb->get_var(
"SELECT
COUNT(DISTINCT coupon_id)
@ -455,7 +425,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*/
public static function update( $order ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
if ( ! $order->get_id() || ! $order->get_date_created() ) {
return -1;
@ -521,20 +491,13 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*/
public static function delete_order( $post_id ) {
global $wpdb;
$order_id = (int) $post_id;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$order_id = (int) $post_id;
if ( 'shop_order' !== get_post_type( $order_id ) && 'shop_order_refund' !== get_post_type( $order_id ) ) {
return;
}
$wpdb->query(
$wpdb->prepare(
"DELETE FROM ${table_name} WHERE order_id = %d",
$order_id
)
);
$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
/**
* Fires when orders stats are deleted.
*
@ -634,7 +597,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*/
protected static function set_customer_first_order( $customer_id, $order_id ) {
global $wpdb;
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
$orders_stats_table = self::get_db_table_name();
$wpdb->query(
$wpdb->prepare(
@ -644,4 +607,18 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
)
);
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
@ -24,7 +25,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_order_product_lookup';
protected static $table_name = 'wc_order_product_lookup';
/**
* Cache identifier.
@ -58,18 +59,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'sku' => 'strval',
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
*
* @var array
*/
protected $report_columns = array(
'product_id' => 'product_id',
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => 'COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN order_id END ) ) as orders_count',
);
/**
* Extended product attributes to include in the data.
*
@ -90,13 +79,23 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* Constructor
* Data store context used to pass to filters.
*
* @var string
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Avoid ambigious column order_id in SQL query.
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
protected $context = 'products';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'product_id' => 'product_id',
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
);
}
/**
@ -110,62 +109,69 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* Fills FROM clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param string $arg_name Name of the FROM sql param.
* @param string $arg_name Target of the JOIN sql param.
* @param string $id_cell ID cell identifier, like `table_name.id_column_name`.
* @return array
*/
protected function get_from_sql_params( $query_args, $arg_name, $id_cell ) {
global $wpdb;
$sql_query['outer_from_clause'] = '';
$type = 'join';
// Order by product name requires extra JOIN.
if ( 'product_name' === $query_args['orderby'] ) {
$sql_query[ $arg_name ] .= " JOIN {$wpdb->prefix}posts AS _products ON {$id_cell} = _products.ID";
switch ( $query_args['orderby'] ) {
case 'product_name':
$join = " JOIN {$wpdb->posts} AS _products ON {$id_cell} = _products.ID";
break;
case 'sku':
$join = " JOIN {$wpdb->postmeta} AS postmeta ON {$id_cell} = postmeta.post_id AND postmeta.meta_key = '_sku'";
break;
case 'variations':
$type = 'left_join';
$join = "LEFT JOIN ( SELECT post_parent, COUNT(*) AS variations FROM {$wpdb->posts} WHERE post_type = 'product_variation' GROUP BY post_parent ) AS _variations ON {$id_cell} = _variations.post_parent";
break;
default:
$join = '';
break;
}
if ( 'sku' === $query_args['orderby'] ) {
$sql_query[ $arg_name ] .= " JOIN {$wpdb->prefix}postmeta AS postmeta ON {$id_cell} = postmeta.post_id AND postmeta.meta_key = '_sku'";
if ( $join ) {
if ( 'inner' === $arg_name ) {
$this->subquery->add_sql_clause( $type, $join );
} else {
$this->add_sql_clause( $type, $join );
}
}
if ( 'variations' === $query_args['orderby'] ) {
$sql_query[ $arg_name ] .= " LEFT JOIN ( SELECT post_parent, COUNT(*) AS variations FROM {$wpdb->prefix}posts WHERE post_type = 'product_variation' GROUP BY post_parent ) AS _variations ON {$id_cell} = _variations.post_parent";
}
return $sql_query;
}
/**
* Updates the database query with parameters used for Products report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$order_product_lookup_table = self::get_db_table_name();
$sql_query_params = $this->get_time_period_sql_params( $query_args, $order_product_lookup_table );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
$this->get_time_period_sql_params( $query_args, $order_product_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->get_order_by_sql_params( $query_args );
$included_products = $this->get_included_products( $query_args );
if ( $included_products ) {
$sql_query_params = array_merge( $sql_query_params, $this->get_from_sql_params( $query_args, 'outer_from_clause', 'default_results.product_id' ) );
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
$this->get_from_sql_params( $query_args, 'outer', 'default_results.product_id' );
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
} else {
$sql_query_params = array_merge( $sql_query_params, $this->get_from_sql_params( $query_args, 'from_clause', "{$order_product_lookup_table}.product_id" ) );
$this->get_from_sql_params( $query_args, 'inner', "{$order_product_lookup_table}.product_id" );
}
$included_variations = $this->get_included_variations( $query_args );
if ( $included_variations ) {
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" );
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )";
$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
}
return $sql_query_params;
}
/**
@ -175,11 +181,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* @return string
*/
protected function normalize_order_by( $order_by ) {
global $wpdb;
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
if ( 'date' === $order_by ) {
return $order_product_lookup_table . '.date_created';
return self::get_db_table_name() . '.date_created';
}
if ( 'product_name' === $order_by ) {
return 'post_title';
@ -261,7 +264,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -287,6 +290,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -295,12 +300,13 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );
$included_products = $this->get_included_products_array( $query_args );
$params = $this->get_limit_params( $query_args );
$this->get_sql_query_params( $query_args );
if ( count( $included_products ) > 0 ) {
$total_results = count( $included_products );
$total_pages = (int) ceil( $total_results / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
if ( 'date' === $query_args['orderby'] ) {
$selections .= ", {$table_name}.date_created";
@ -309,59 +315,43 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, array( 'product_id' ) );
$ids_table = $this->get_ids_table( $included_products, 'product_id' );
$prefix = "SELECT {$join_selections} FROM (";
$suffix = ") AS {$table_name}";
$right_join = "RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.product_id = {$table_name}.product_id";
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->add_sql_clause( 'select', $join_selections );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.product_id = {$table_name}.product_id"
);
$products_query = $this->get_query_statement();
} else {
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT
product_id
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
product_id
) AS tt"
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) ) {
return $data;
}
$prefix = '';
$suffix = '';
$right_join = '';
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$products_query = $this->subquery->get_query_statement();
}
$product_data = $wpdb->get_results(
"${prefix}
SELECT
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
product_id
{$suffix}
{$right_join}
{$sql_query_params['outer_from_clause']}
ORDER BY
{$sql_query_params['order_by_clause']}
{$sql_query_params['limit']}
",
$products_query,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -433,7 +423,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
$result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME,
self::get_db_table_name(),
array(
'order_item_id' => $order_item_id,
'order_id' => $order->get_id(),
@ -490,14 +480,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public static function sync_on_order_delete( $order_id ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM ${table_name} WHERE order_id = %d",
$order_id
)
);
$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
/**
* Fires when product's reports are removed from database.
@ -509,4 +492,15 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
ReportsCache::invalidate();
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', 'product_id' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', 'product_id' );
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Products\Stats\DataStore.
@ -35,42 +36,37 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
);
/**
* SQL columns to select in the db query.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => 'COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN order_id END ) ) as orders_count',
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
'variations_count' => 'COUNT(DISTINCT variation_id) as variations_count',
);
protected $context = 'product_stats';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Avoid ambigious column order_id in SQL query.
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
'variations_count' => 'COUNT(DISTINCT variation_id) as variations_count',
);
}
/**
* Updates the database query with parameters used for Products Stats report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @param array $totals_params SQL parameters for the totals query.
* @param array $intervals_params SQL parameters for the intervals query.
*/
protected function update_sql_query_params( $query_args, &$totals_params, &$intervals_params ) {
protected function update_sql_query_params( $query_args ) {
global $wpdb;
$products_where_clause = '';
$products_from_clause = '';
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$products_where_clause = '';
$products_from_clause = '';
$order_product_lookup_table = self::get_db_table_name();
$included_products = $this->get_included_products( $query_args );
if ( $included_products ) {
@ -88,13 +84,14 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
$products_where_clause .= " AND ( {$order_status_filter} )";
}
$totals_params = array_merge( $totals_params, $this->get_time_period_sql_params( $query_args, $order_product_lookup_table ) );
$totals_params['where_clause'] .= $products_where_clause;
$totals_params['from_clause'] .= $products_from_clause;
$this->get_time_period_sql_params( $query_args, $order_product_lookup_table );
$this->total_query->add_sql_clause( 'where', $products_where_clause );
$this->total_query->add_sql_clause( 'join', $products_from_clause );
$intervals_params = array_merge( $intervals_params, $this->get_intervals_sql_params( $query_args, $order_product_lookup_table ) );
$intervals_params['where_clause'] .= $products_where_clause;
$intervals_params['from_clause'] .= $products_from_clause;
$this->get_intervals_sql_params( $query_args, $order_product_lookup_table );
$this->interval_query->add_sql_clause( 'where', $products_where_clause );
$this->interval_query->add_sql_clause( 'join', $products_from_clause );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
}
/**
@ -107,7 +104,7 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -133,47 +130,50 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$selections = $this->selected_columns( $query_args );
$totals_query = array();
$intervals_query = array();
$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$this->update_sql_query_params( $query_args );
$this->get_limit_sql_params( $query_args );
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
$db_intervals = $wpdb->get_col(
"SELECT
{$intervals_query['select_clause']} AS time_interval
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval"
); // WPCS: cache ok, DB call ok, , unprepared SQL ok.
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$db_interval_count = count( $db_intervals );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] );
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return array();
}
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name );
$intervals = array();
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
$totals = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}",
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// @todo remove these assignements when refactoring segmenter classes to use query objects.
$totals_query = array(
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
);
$intervals_query = array(
'select_clause' => $this->get_sql_clause( 'select' ),
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'order_by' => $this->get_sql_clause( 'order_by' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$segmenter = new Segmenter( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
@ -181,27 +181,15 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
return new \WP_Error( 'woocommerce_reports_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce-admin' ) );
}
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$selections = ', ' . $selections;
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$intervals = $wpdb->get_results(
"SELECT
MAX(${table_name}.date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval
{$selections}
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval
ORDER BY
{$intervals_query['order_by_clause']}
{$intervals_query['limit']}",
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -219,10 +207,10 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
'page_no' => (int) $query_args['page'],
);
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}
@ -248,4 +236,18 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
return $order_by;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}

View File

@ -98,11 +98,15 @@ class Segmenter {
$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
}
$segment_labels = $this->get_segment_labels();
foreach ( $segments_db_result as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
$segment_labels = $this->get_segment_labels();
$segment_id = $segment_data[ $segment_dimension ];
if ( ! isset( $segment_labels[ $segment_id ] ) ) {
continue;
}
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
$segment_datum = array(
'segment_id' => $segment_id,
'segment_label' => $segment_labels[ $segment_id ],
'subtotals' => $segment_data,
@ -292,6 +296,11 @@ class Segmenter {
$segment_labels = $this->get_segment_labels();
foreach ( $segments_db_result as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
if ( ! isset( $segment_labels[ $segment_id ] ) ) {
continue;
}
$time_interval = $segment_data['time_interval'];
if ( ! isset( $aggregated_segment_result[ $time_interval ] ) ) {
$aggregated_segment_result[ $time_interval ] = array();
@ -299,7 +308,6 @@ class Segmenter {
}
unset( $segment_data['time_interval'] );
unset( $segment_data['datetime_anchor'] );
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
'segment_label' => $segment_labels[ $segment_id ],
@ -407,7 +415,8 @@ class Segmenter {
if ( isset( $this->query_args['coupons'] ) ) {
$args['include'] = $this->query_args['coupons'];
}
$coupons = CouponsDataStore::get_coupons( $args );
$coupons_store = new CouponsDataStore();
$coupons = $coupons_store->get_coupons( $args );
$segments = wp_list_pluck( $coupons, 'ID' );
$segment_labels = wp_list_pluck( $coupons, 'post_title', 'ID' );
$segment_labels = array_map( 'wc_format_coupon_code', $segment_labels );

View File

@ -0,0 +1,220 @@
<?php
/**
* Admin\API\Reports\SqlQuery class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Admin\API\Reports\SqlQuery: Common parent for manipulating SQL query clauses.
*/
class SqlQuery {
/**
* List of SQL clauses.
*
* @var array
*/
private $sql_clauses = array(
'select' => array(),
'from' => array(),
'left_join' => array(),
'join' => array(),
'right_join' => array(),
'where' => array(),
'where_time' => array(),
'group_by' => array(),
'having' => array(),
'limit' => array(),
'order_by' => array(),
);
/**
* SQL clause merge filters.
*
* @var array
*/
private $sql_filters = array(
'where' => array(
'where',
'where_time',
),
'join' => array(
'right_join',
'join',
'left_join',
),
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context;
/**
* Constructor.
*
* @param string $context Optional context passed to filters. Default empty string.
*/
public function __construct( $context = '' ) {
$this->context = $context;
}
/**
* Add a SQL clause to be included when get_data is called.
*
* @param string $type Clause type.
* @param string $clause SQL clause.
*/
protected function add_sql_clause( $type, $clause ) {
if ( isset( $this->sql_clauses[ $type ] ) && ! empty( $clause ) ) {
$this->sql_clauses[ $type ][] = $clause;
}
}
/**
* Get SQL clause by type.
*
* @param string $type Clause type.
* @param string $handling Whether to filter the return value (filtered|unfiltered). Default unfiltered.
*
* @return string SQL clause.
*/
protected function get_sql_clause( $type, $handling = 'unfiltered' ) {
if ( ! isset( $this->sql_clauses[ $type ] ) ) {
return '';
}
/**
* Default to bypassing filters for clause retrieval internal to data stores.
* The filters are applied when the full SQL statement is retrieved.
*/
if ( 'unfiltered' === $handling ) {
return implode( ' ', $this->sql_clauses[ $type ] );
}
if ( isset( $this->sql_filters[ $type ] ) ) {
$clauses = array();
foreach( $this->sql_filters[ $type ] as $subset ) {
$clauses = array_merge( $clauses, $this->sql_clauses[ $subset ] );
}
} else {
$clauses = $this->sql_clauses[ $type ];
}
/**
* Filter SQL clauses by type and context.
*
* @param array $clauses The original arguments for the request.
* @param string $context The data store context.
*/
$clauses = apply_filters( "wc_admin_clauses_{$type}", $clauses, $this->context );
/**
* Filter SQL clauses by type and context.
*
* @param array $clauses The original arguments for the request.
*/
$clauses = apply_filters( "wc_admin_clauses_{$type}_{$this->context}", $clauses );
return implode( ' ', $clauses );
}
/**
* Clear SQL clauses by type.
*
* @param string|array $types Clause type.
*/
protected function clear_sql_clause( $types ) {
foreach ( (array) $types as $type ) {
if ( isset( $this->sql_clauses[ $type ] ) ) {
$this->sql_clauses[ $type ] = array();
}
}
}
/**
* Replace strings within SQL clauses by type.
*
* @param string $type Clause type.
* @param string $search String to search for.
* @param string $replace Replacement string.
*/
protected function str_replace_clause( $type, $search, $replace ) {
if ( isset( $this->sql_clauses[ $type ] ) ) {
foreach ( $this->sql_clauses[ $type ] as $key => $sql ) {
$this->sql_clauses[ $type ][ $key ] = str_replace( $search, $replace, $sql );
}
}
}
/**
* Get the full SQL statement.
*
* @return string
*/
public function get_query_statement() {
$join = $this->get_sql_clause( 'join', 'filtered' );
$where = $this->get_sql_clause( 'where', 'filtered' );
$group_by = $this->get_sql_clause( 'group_by', 'filtered' );
$having = $this->get_sql_clause( 'having', 'filtered' );
$order_by = $this->get_sql_clause( 'order_by', 'filtered' );
$statement = "
SELECT
{$this->get_sql_clause( 'select', 'filtered' )}
FROM
{$this->get_sql_clause( 'from', 'filtered' )}
{$join}
WHERE
1=1
{$where}
";
if ( ! empty( $group_by ) ) {
$statement .= "
GROUP BY
{$group_by}
";
if ( ! empty( $having ) ) {
$statement .= "
HAVING
1=1
{$having}
";
}
}
if ( ! empty( $order_by ) ) {
$statement .= "
ORDER BY
{$order_by}
";
}
return $statement . $this->get_sql_clause( 'limit', 'filtered' );
}
/**
* Reinitialize the clause array.
*/
public function clear_all_clauses() {
$this->sql_clauses = array(
'select' => array(),
'from' => array(),
'left_join' => array(),
'join' => array(),
'right_join' => array(),
'where' => array(),
'where_time' => array(),
'group_by' => array(),
'having' => array(),
'limit' => array(),
'order_by' => array(),
);
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
@ -24,7 +25,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_order_tax_lookup';
protected static $table_name = 'wc_order_tax_lookup';
/**
* Cache identifier.
@ -52,32 +53,29 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array(
'tax_rate_id' => 'tax_rate_id',
'name' => 'tax_rate_name as name',
'tax_rate' => 'tax_rate',
'country' => 'tax_rate_country as country',
'state' => 'tax_rate_state as state',
'priority' => 'tax_rate_priority as priority',
'total_tax' => 'SUM(total_tax) as total_tax',
'order_tax' => 'SUM(order_tax) as order_tax',
'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
'orders_count' => 'COUNT( DISTINCT ( CASE WHEN total_tax >= 0 THEN order_id END ) ) as orders_count',
);
protected $context = 'taxes';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Avoid ambigious columns in SQL query.
$this->report_columns['tax_rate_id'] = $table_name . '.' . $this->report_columns['tax_rate_id'];
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'tax_rate_id' => "{$table_name}.tax_rate_id",
'name' => 'tax_rate_name as name',
'tax_rate' => 'tax_rate',
'country' => 'tax_rate_country as country',
'state' => 'tax_rate_state as state',
'priority' => 'tax_rate_priority as priority',
'total_tax' => 'SUM(total_tax) as total_tax',
'order_tax' => 'SUM(order_tax) as order_tax',
'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
'orders_count' => "COUNT( DISTINCT ( CASE WHEN total_tax >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
);
}
/**
@ -92,76 +90,46 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @param array $query_args Query arguments supplied by the user.
* @param string $order_status_filter Order status subquery.
* @return array Array of parameters used for SQL query.
*/
protected function get_from_sql_params( $query_args, $order_status_filter ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$sql_query['from_clause'] = '';
$sql_query['outer_from_clause'] = '';
$table_name = self::get_db_table_name();
if ( $order_status_filter ) {
$sql_query['from_clause'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
}
if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
$sql_query['outer_from_clause'] .= " JOIN {$wpdb->prefix}woocommerce_tax_rates ON default_results.tax_rate_id = {$wpdb->prefix}woocommerce_tax_rates.tax_rate_id";
$this->add_sql_clause( 'join', "JOIN {$wpdb->prefix}woocommerce_tax_rates ON default_results.tax_rate_id = {$wpdb->prefix}woocommerce_tax_rates.tax_rate_id" );
} else {
$sql_query['from_clause'] .= " JOIN {$wpdb->prefix}woocommerce_tax_rates ON {$table_name}.tax_rate_id = {$wpdb->prefix}woocommerce_tax_rates.tax_rate_id";
$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}woocommerce_tax_rates ON {$table_name}.tax_rate_id = {$wpdb->prefix}woocommerce_tax_rates.tax_rate_id" );
}
return $sql_query;
}
/**
* Updates the database query with parameters used for Taxes report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$order_tax_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$order_tax_lookup_table = self::get_db_table_name();
$sql_query_params = $this->get_time_period_sql_params( $query_args, $order_tax_lookup_table );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
$this->get_time_period_sql_params( $query_args, $order_tax_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->get_order_by_sql_params( $query_args );
$order_status_filter = $this->get_status_subquery( $query_args );
$sql_query_params = array_merge( $sql_query_params, $this->get_from_sql_params( $query_args, $order_status_filter ) );
$this->get_from_sql_params( $query_args, $order_status_filter );
if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
$allowed_taxes = implode( ',', $query_args['taxes'] );
$sql_query_params['where_clause'] .= " AND {$order_tax_lookup_table}.tax_rate_id IN ({$allowed_taxes})";
$allowed_taxes = self::get_filtered_ids( $query_args, 'taxes' );
$this->subquery->add_sql_clause( 'where', "AND {$order_tax_lookup_table}.tax_rate_id IN ({$allowed_taxes})" );
}
if ( $order_status_filter ) {
$sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )";
$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
}
return $sql_query_params;
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_order_by_sql_params( $query_args ) {
$sql_query['order_by_clause'] = '';
if ( isset( $query_args['orderby'] ) ) {
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
}
if ( isset( $query_args['order'] ) ) {
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
} else {
$sql_query['order_by_clause'] .= ' DESC';
}
return $sql_query;
}
/**
@ -173,7 +141,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -197,6 +165,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -204,11 +174,12 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'page_no' => 0,
);
$sql_query_params = $this->get_sql_query_params( $query_args );
$this->get_sql_query_params( $query_args );
$params = $this->get_limit_params( $query_args );
if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
$total_results = count( $query_args['taxes'] );
$total_pages = (int) ceil( $total_results / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
$inner_selections = array( 'tax_rate_id', 'total_tax', 'order_tax', 'shipping_tax', 'orders_count' );
$outer_selections = array( 'name', 'tax_rate', 'country', 'state', 'priority' );
@ -217,61 +188,42 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, array( 'tax_rate_id' ), $outer_selections );
$ids_table = $this->get_ids_table( $query_args['taxes'], 'tax_rate_id' );
$prefix = "SELECT {$join_selections} FROM (";
$suffix = ") AS {$table_name}";
$right_join = "RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.tax_rate_id = {$table_name}.tax_rate_id";
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( array( 'fields' => $inner_selections ) ) );
$this->add_sql_clause( 'select', $join_selections );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.tax_rate_id = {$table_name}.tax_rate_id"
);
$taxes_query = $this->get_query_statement();
} else {
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT
{$table_name}.tax_rate_id
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
{$table_name}.tax_rate_id
) AS tt"
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$selections = $this->selected_columns( $query_args );
$prefix = '';
$suffix = '';
$right_join = '';
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$taxes_query = $this->subquery->get_query_statement();
}
$tax_data = $wpdb->get_results(
"{$prefix}
SELECT
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
{$table_name}.tax_rate_id
{$suffix}
{$right_join}
{$sql_query_params['outer_from_clause']}
ORDER BY
{$sql_query_params['order_by_clause']}
{$sql_query_params['limit']}
",
$taxes_query,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -330,7 +282,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
foreach ( $tax_items as $tax_item ) {
$result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME,
self::get_db_table_name(),
array(
'order_id' => $order->get_id(),
'date_created' => $order->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ),
@ -372,14 +324,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public static function sync_on_order_delete( $order_id ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM ${table_name} WHERE order_id = %d",
$order_id
)
);
$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
/**
* Fires when tax's reports are removed from database.
@ -391,4 +336,15 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
ReportsCache::invalidate();
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', self::get_db_table_name() . '.tax_rate_id' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', self::get_db_table_name() . '.tax_rate_id' );
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Taxes\Stats\DataStore.
@ -23,7 +24,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_order_tax_lookup';
protected static $table_name = 'wc_order_tax_lookup';
/**
* Cache identifier.
@ -46,81 +47,72 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* SQL columns to select in the db query.
* Data store context used to pass to filters.
*
* @var array
* @var string
*/
protected $report_columns = array(
'tax_codes' => 'COUNT(DISTINCT tax_rate_id) as tax_codes',
'total_tax' => 'SUM(total_tax) AS total_tax',
'order_tax' => 'SUM(order_tax) as order_tax',
'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
'orders_count' => 'COUNT( DISTINCT ( CASE WHEN parent_id = 0 THEN order_id END ) ) as orders_count',
);
protected $context = 'tax_stats';
/**
* Constructor
* Assign report columns once full table name has been assigned.
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Avoid ambigious column order_id in SQL query.
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'tax_codes' => 'COUNT(DISTINCT tax_rate_id) as tax_codes',
'total_tax' => 'SUM(total_tax) AS total_tax',
'order_tax' => 'SUM(order_tax) as order_tax',
'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
'orders_count' => "COUNT( DISTINCT ( CASE WHEN parent_id = 0 THEN {$table_name}.order_id END ) ) as orders_count",
);
}
/**
* Updates the database query with parameters used for Taxes report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$order_tax_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$order_tax_lookup_table = self::get_db_table_name();
$sql_query_params = $this->get_time_period_sql_params( $query_args, $order_tax_lookup_table );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
$this->get_time_period_sql_params( $query_args, $order_tax_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->get_order_by_sql_params( $query_args );
if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
$allowed_taxes = implode( ',', $query_args['taxes'] );
$sql_query_params['where_clause'] .= " AND {$order_tax_lookup_table}.tax_rate_id IN ({$allowed_taxes})";
$allowed_taxes = self::get_filtered_ids( $query_args, 'taxes' );
$this->interval_query->add_sql_clause( 'where', "AND {$order_tax_lookup_table}.tax_rate_id IN ({$allowed_taxes})" );
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )";
$this->interval_query->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
}
return $sql_query_params;
}
/**
* Updates the database query with parameters used for Taxes Stats report
*
* @param array $query_args Query arguments supplied by the user.
* @param array $totals_params SQL parameters for the totals query.
* @param array $intervals_params SQL parameters for the intervals query.
*/
protected function update_sql_query_params( $query_args, &$totals_params, &$intervals_params ) {
global $wpdb;
$taxes_where_clause = '';
$taxes_from_clause = '';
$order_tax_lookup_table = $wpdb->prefix . self::TABLE_NAME;
protected function update_sql_query_params( $query_args ) {
$taxes_where_clause = '';
$order_tax_lookup_table = self::get_db_table_name();
if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
$allowed_taxes = implode( ',', $query_args['taxes'] );
$taxes_where_clause .= " AND {$order_tax_lookup_table}.tax_rate_id IN ({$allowed_taxes})";
}
$totals_params = array_merge( $totals_params, $this->get_time_period_sql_params( $query_args, $order_tax_lookup_table ) );
$totals_params['where_clause'] .= $taxes_where_clause;
$this->get_time_period_sql_params( $query_args, $order_tax_lookup_table );
$this->total_query->add_sql_clause( 'where', $taxes_where_clause );
$intervals_params = array_merge( $intervals_params, $this->get_intervals_sql_params( $query_args, $order_tax_lookup_table ) );
$intervals_params['where_clause'] .= $taxes_where_clause;
$this->get_intervals_sql_params( $query_args, $order_tax_lookup_table );
$this->interval_query->add_sql_clause( 'where', $taxes_where_clause );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
}
/**
@ -156,7 +148,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -180,6 +172,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'totals' => (object) array(),
'intervals' => (object) array(),
@ -188,81 +182,62 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$totals_query = array();
$intervals_query = array();
$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$order_stats_join = "JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$this->update_sql_query_params( $query_args );
$this->interval_query->add_sql_clause( 'join', $order_stats_join );
$db_intervals = $wpdb->get_col(
"SELECT
{$intervals_query['select_clause']} AS time_interval
FROM
{$table_name}
{$intervals_query['from_clause']}
JOIN
{$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval"
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$db_interval_count = count( $db_intervals );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] );
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'join', $order_stats_join );
$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
$totals = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$totals_query['from_clause']}
JOIN
{$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}",
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_reports_taxes_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce-admin' ) );
}
// @todo remove these assignements when refactoring segmenter classes to use query objects.
$totals_query = array(
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
);
$intervals_query = array(
'select_clause' => $this->get_sql_clause( 'select' ),
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
);
$segmenter = new Segmenter( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
if ( '' !== $selections ) {
$selections = ', ' . $selections;
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$intervals = $wpdb->get_results(
"SELECT
MAX({$table_name}.date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval
{$selections}
FROM
{$table_name}
{$intervals_query['from_clause']}
JOIN
{$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
GROUP BY
time_interval
ORDER BY
{$intervals_query['order_by_clause']}
{$intervals_query['limit']}",
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -280,19 +255,31 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'page_no' => (int) $query_args['page'],
);
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}
$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
$this->create_interval_subtotals( $data->intervals );
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}

View File

@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Variations\DataStore.
@ -23,7 +24,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*
* @var string
*/
const TABLE_NAME = 'wc_order_product_lookup';
protected static $table_name = 'wc_order_product_lookup';
/**
* Cache identifier.
@ -52,19 +53,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'sku' => 'strval',
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
*
* @var array
*/
protected $report_columns = array(
'product_id' => 'product_id',
'variation_id' => 'variation_id',
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
);
/**
* Extended product attributes to include in the data.
*
@ -82,72 +70,83 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
/**
* Constructor
* Data store context used to pass to filters.
*
* @var string
*/
public function __construct() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Avoid ambigious column order_id in SQL query.
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
protected $context = 'variations';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'product_id' => 'product_id',
'variation_id' => 'variation_id',
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
);
}
/**
* Fills FROM clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param string $arg_name Name of the FROM sql param.
* @return array
* @param string $arg_name Target of the JOIN sql param.
*/
protected function get_from_sql_params( $query_args, $arg_name ) {
global $wpdb;
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$sql_query['from_clause'] = '';
$sql_query['outer_from_clause'] = '';
if ( 'sku' === $query_args['orderby'] ) {
$sql_query[ $arg_name ] .= " JOIN {$wpdb->prefix}postmeta AS postmeta ON {$order_product_lookup_table}.variation_id = postmeta.post_id AND postmeta.meta_key = '_sku'";
if ( 'sku' !== $query_args['orderby'] ) {
return;
}
return $sql_query;
$table_name = self::get_db_table_name();
$join = "JOIN {$wpdb->postmeta} AS postmeta ON {$table_name}.variation_id = postmeta.post_id AND postmeta.meta_key = '_sku'";
if ( 'inner' === $arg_name ) {
$this->subquery->add_sql_clause( 'join', $join );
} else {
$this->add_sql_clause( 'join', $join );
}
}
/**
* Updates the database query with parameters used for Products report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$order_product_lookup_table = self::get_db_table_name();
$sql_query_params = $this->get_time_period_sql_params( $query_args, $order_product_lookup_table );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
$this->get_time_period_sql_params( $query_args, $order_product_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->get_order_by_sql_params( $query_args );
if ( count( $query_args['variations'] ) > 0 ) {
$sql_query_params = array_merge( $sql_query_params, $this->get_from_sql_params( $query_args, 'outer_from_clause' ) );
$this->get_from_sql_params( $query_args, 'outer' );
} else {
$sql_query_params = array_merge( $sql_query_params, $this->get_from_sql_params( $query_args, 'from_clause' ) );
$this->get_from_sql_params( $query_args, 'inner' );
}
$included_products = $this->get_included_products( $query_args );
if ( $included_products ) {
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
}
if ( count( $query_args['variations'] ) > 0 ) {
$allowed_variations_str = implode( ',', $query_args['variations'] );
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.variation_id IN ({$allowed_variations_str})";
$allowed_variations_str = self::get_filtered_ids( $query_args, 'variations' );
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$allowed_variations_str})" );
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )";
$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
}
return $sql_query_params;
}
/**
@ -158,11 +157,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* @return string
*/
protected function normalize_order_by( $order_by ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
if ( 'date' === $order_by ) {
return $table_name . '.date_created';
return self::get_db_table_name() . '.date_created';
}
if ( 'sku' === $order_by ) {
return 'meta_value';
@ -237,7 +233,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -263,6 +259,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
@ -270,78 +268,56 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$included_products = $this->get_included_products_array( $query_args );
$this->get_sql_query_params( $query_args );
$params = $this->get_limit_params( $query_args );
if ( count( $included_products ) > 0 && count( $query_args['variations'] ) > 0 ) {
$sql_query_params = $this->get_sql_query_params( $query_args );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
if ( 'date' === $query_args['orderby'] ) {
$selections .= ", {$table_name}.date_created";
$this->subquery->add_sql_clause( 'select', ", {$table_name}.date_created" );
}
$total_results = count( $query_args['variations'] );
$total_pages = (int) ceil( $total_results / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, array( 'product_id', 'variation_id' ) );
$ids_table = $this->get_ids_table( $query_args['variations'], 'variation_id', array( 'product_id' => $included_products[0] ) );
$prefix = "SELECT {$join_selections} FROM (";
$suffix = ") AS {$table_name}";
$right_join = "RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.variation_id = {$table_name}.variation_id";
} else {
$sql_query_params = $this->get_sql_query_params( $query_args );
$this->add_sql_clause( 'select', $join_selections );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.variation_id = {$table_name}.variation_id"
);
$variations_query = $this->get_query_statement();
} else {
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT
product_id
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
product_id, variation_id
) AS tt"
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$prefix = '';
$suffix = '';
$right_join = '';
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$variations_query = $this->subquery->get_query_statement();
}
$product_data = $wpdb->get_results(
"{$prefix}
SELECT
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
product_id, variation_id
{$suffix}
{$right_join}
{$sql_query_params['outer_from_clause']}
ORDER BY
{$sql_query_params['order_by_clause']}
{$sql_query_params['limit']}
",
$variations_query,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
@ -364,4 +340,15 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return $data;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', 'product_id' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', 'product_id, variation_id' );
}
}

View File

@ -137,7 +137,7 @@ class ReportCSVEmail extends \WC_Email {
$this->download_url = $download_url;
if ( isset( $this->report_labels[ $report_type ] ) ) {
$this->report_type = $this->report_labels[ $report_type ];
$this->report_type = $this->report_labels[ $report_type ];
$this->placeholders['{report_name}'] = $this->report_type;
}

View File

@ -42,7 +42,7 @@ class WC_Tests_API_Init extends WC_REST_Unit_Test_Case {
if (
0 === strpos( $query, 'REPLACE INTO' ) &&
false !== strpos( $query, OrdersStatsDataStore::TABLE_NAME )
false !== strpos( $query, OrdersStatsDataStore::get_db_table_name() )
) {
remove_filter( 'query', array( $this, 'filter_order_query' ) );
return "DESCRIBE $wpdb->posts"; // Execute any random query.

View File

@ -17,10 +17,10 @@ class WC_Helper_Reports {
*/
public static function reset_stats_dbs() {
global $wpdb;
$wpdb->query( "DELETE FROM $wpdb->prefix" . \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::TABLE_NAME ); // @codingStandardsIgnoreLine.
$wpdb->query( "DELETE FROM $wpdb->prefix" . \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore::TABLE_NAME ); // @codingStandardsIgnoreLine.
$wpdb->query( "DELETE FROM $wpdb->prefix" . \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore::TABLE_NAME ); // @codingStandardsIgnoreLine.
$wpdb->query( "DELETE FROM $wpdb->prefix" . \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::TABLE_NAME ); // @codingStandardsIgnoreLine.
$wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine.
$wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine.
$wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine.
$wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine.
\Automattic\WooCommerce\Admin\CategoryLookup::instance()->regenerate();
}
}