Skip to content

Commit

Permalink
Make products and rates endpoints return zero-value items (woocommerc…
Browse files Browse the repository at this point in the history
…e#1722)

* Make products endpoint return zero-value items

* Make rates endpoint return zero-value items

* Several fixes

* Add tests

* Simplify code

* Merge both queries into one to simplify code

* Rename 'get_outer_from_sql_params' to 'get_from_sql_params'
  • Loading branch information
Aljullu authored Mar 6, 2019
1 parent 9e83beb commit 968d85e
Show file tree
Hide file tree
Showing 5 changed files with 392 additions and 81 deletions.
67 changes: 62 additions & 5 deletions includes/data-stores/class-wc-admin-reports-data-store.php
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,53 @@ protected function get_limit_sql_params( $query_args ) {
return $sql_query;
}

/**
* Generates a virtual table given a list of IDs.
*
* @param array $ids Array of IDs.
* @return array
*/
protected function get_ids_table( $ids ) {
$selects = array();
foreach ( $ids as $id ) {
array_push( $selects, "SELECT {$id} AS id" );
}
return join( ' UNION ', $selects );
}

/**
* Returns a comma separated list of the fields in the `query_args`, if there aren't, returns `report_columns` keys.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_fields( $query_args ) {
if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) {
return $query_args['fields'];
}
return array_keys( $this->report_columns );
}

/**
* Returns a comma separated list of the field names prepared to be used for a selection after a join with `default_results`.
*
* @param array $fields Array of fields name.
* @param string $id_field Name of the column used as an identifier.
* @param array $outer_selections Array of fields that are not selected in the inner query.
* @return string
*/
protected function format_join_selections( $fields, $id_field, $outer_selections = array() ) {
foreach ( $fields as $i => $field ) {
if ( $field === $id_field ) {
$fields[ $i ] = "default_results.id AS {$field}";
}
if ( in_array( $field, $outer_selections, true ) && array_key_exists( $field, $this->report_columns ) ) {
$fields[ $i ] = $this->report_columns[ $field ];
}
}
return implode( ', ', $fields );
}

/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
Expand Down Expand Up @@ -683,12 +730,12 @@ protected function get_products_by_cat_ids( $categories ) {
}

/**
* Returns comma separated ids of allowed products, based on query arguments from the user.
* Returns an array of ids of allowed products, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
* @return array
*/
protected function get_included_products( $query_args ) {
protected function get_included_products_array( $query_args ) {
$included_products = array();
$operator = $this->get_match_operator( $query_args );

Expand All @@ -710,8 +757,18 @@ protected function get_included_products( $query_args ) {
}
}

$included_products_str = implode( ',', $included_products );
return $included_products_str;
return $included_products;
}

/**
* Returns comma separated ids of allowed products, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_products( $query_args ) {
$included_products = $this->get_included_products_array( $query_args );
return implode( ',', $included_products );
}

/**
Expand Down
119 changes: 82 additions & 37 deletions includes/data-stores/class-wc-admin-reports-products-data-store.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,6 @@ protected function get_order_by_sql_params( $query_args ) {
if ( isset( $query_args['orderby'] ) ) {
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
}
// Order by product name requires extra JOIN.
if ( false !== strpos( $sql_query['order_by_clause'], '_products' ) ) {
$sql_query['from_clause'] .= " JOIN {$wpdb->prefix}posts AS _products ON {$order_product_lookup_table}.product_id = _products.ID";
}

if ( 'postmeta.meta_value' === $sql_query['order_by_clause'] ) {
$sql_query['from_clause'] .= " JOIN {$wpdb->prefix}postmeta AS postmeta ON {$order_product_lookup_table}.product_id = postmeta.post_id AND postmeta.meta_key = '_sku'";
}

if ( 'variations' === $sql_query['order_by_clause'] ) {
$sql_query['from_clause'] .= " LEFT JOIN ( SELECT post_parent, COUNT(*) AS variations FROM {$wpdb->prefix}posts WHERE post_type = 'product_variation' GROUP BY post_parent ) AS _variations ON {$order_product_lookup_table}.product_id = _variations.post_parent";
}

if ( isset( $query_args['order'] ) ) {
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
Expand All @@ -128,6 +116,32 @@ protected function get_order_by_sql_params( $query_args ) {
return $sql_query;
}

/**
* 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 $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'] = '';

// 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";
}
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 ( '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.
*
Expand All @@ -144,7 +158,10 @@ protected function get_sql_query_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.id' ) );
$sql_query_params['where_clause'] .= " 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" ) );
}

$included_variations = $this->get_included_variations( $query_args );
Expand Down Expand Up @@ -175,10 +192,10 @@ protected function normalize_order_by( $order_by ) {
return $order_product_lookup_table . '.date_created';
}
if ( 'product_name' === $order_by ) {
return '_products.post_title';
return 'post_title';
}
if ( 'sku' === $order_by ) {
return 'postmeta.meta_value';
return 'meta_value';
}
return $order_by;
}
Expand Down Expand Up @@ -257,32 +274,57 @@ public function get_data( $query_args ) {
'page_no' => 0,
);

$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );

$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"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$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 );

$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
if ( count( $included_products ) > 0 ) {
$total_results = count( $included_products );
$total_pages = (int) ceil( $total_results / $sql_query_params['per_page'] );

if ( 'date' === $query_args['orderby'] ) {
$selections .= ", {$table_name}.date_created";
}

$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, '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.id = {$table_name}.product_id";
} 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"
); // 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'] );

if ( ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) ) {
return $data;
}

$prefix = '';
$suffix = '';
$right_join = '';
}

$product_data = $wpdb->get_results(
"SELECT
"${prefix}
SELECT
{$selections}
FROM
{$table_name}
Expand All @@ -293,6 +335,9 @@ public function get_data( $query_args ) {
{$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']}
Expand All @@ -309,7 +354,7 @@ public function get_data( $query_args ) {
$product_data = array_map( array( $this, 'cast_numbers' ), $product_data );
$data = (object) array(
'data' => $product_data,
'total' => $db_records_count,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
Expand Down
Loading

0 comments on commit 968d85e

Please sign in to comment.