DataStore.php
537 lines
| 1 | <?php |
| 2 | /** |
| 3 | * API\Reports\Variations\DataStore class file. |
| 4 | */ |
| 5 | |
| 6 | namespace Automattic\WooCommerce\Admin\API\Reports\Variations; |
| 7 | |
| 8 | defined( 'ABSPATH' ) || exit; |
| 9 | |
| 10 | use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore; |
| 11 | use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; |
| 12 | use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; |
| 13 | |
| 14 | /** |
| 15 | * API\Reports\Variations\DataStore. |
| 16 | */ |
| 17 | class DataStore extends ReportsDataStore implements DataStoreInterface { |
| 18 | |
| 19 | /** |
| 20 | * Table used to get the data. |
| 21 | * |
| 22 | * @override ReportsDataStore::$table_name |
| 23 | * |
| 24 | * @var string |
| 25 | */ |
| 26 | protected static $table_name = 'wc_order_product_lookup'; |
| 27 | |
| 28 | /** |
| 29 | * Cache identifier. |
| 30 | * |
| 31 | * @override ReportsDataStore::$cache_key |
| 32 | * |
| 33 | * @var string |
| 34 | */ |
| 35 | protected $cache_key = 'variations'; |
| 36 | |
| 37 | /** |
| 38 | * Mapping columns to data type to return correct response types. |
| 39 | * |
| 40 | * @override ReportsDataStore::$column_types |
| 41 | * |
| 42 | * @var array |
| 43 | */ |
| 44 | protected $column_types = array( |
| 45 | 'date_start' => 'strval', |
| 46 | 'date_end' => 'strval', |
| 47 | 'product_id' => 'intval', |
| 48 | 'variation_id' => 'intval', |
| 49 | 'items_sold' => 'intval', |
| 50 | 'net_revenue' => 'floatval', |
| 51 | 'orders_count' => 'intval', |
| 52 | 'name' => 'strval', |
| 53 | 'price' => 'floatval', |
| 54 | 'image' => 'strval', |
| 55 | 'permalink' => 'strval', |
| 56 | 'sku' => 'strval', |
| 57 | ); |
| 58 | |
| 59 | /** |
| 60 | * Extended product attributes to include in the data. |
| 61 | * |
| 62 | * @var array |
| 63 | */ |
| 64 | protected $extended_attributes = array( |
| 65 | 'name', |
| 66 | 'price', |
| 67 | 'image', |
| 68 | 'permalink', |
| 69 | 'stock_status', |
| 70 | 'stock_quantity', |
| 71 | 'low_stock_amount', |
| 72 | 'sku', |
| 73 | ); |
| 74 | |
| 75 | /** |
| 76 | * Data store context used to pass to filters. |
| 77 | * |
| 78 | * @override ReportsDataStore::$context |
| 79 | * |
| 80 | * @var string |
| 81 | */ |
| 82 | protected $context = 'variations'; |
| 83 | |
| 84 | /** |
| 85 | * Assign report columns once full table name has been assigned. |
| 86 | * |
| 87 | * @override ReportsDataStore::assign_report_columns() |
| 88 | */ |
| 89 | protected function assign_report_columns() { |
| 90 | $table_name = self::get_db_table_name(); |
| 91 | $this->report_columns = array( |
| 92 | 'product_id' => 'product_id', |
| 93 | 'variation_id' => 'variation_id', |
| 94 | 'items_sold' => 'SUM(product_qty) as items_sold', |
| 95 | 'net_revenue' => 'SUM(product_net_revenue) AS net_revenue', |
| 96 | 'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count", |
| 97 | ); |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Fills FROM clause of SQL request based on user supplied parameters. |
| 102 | * |
| 103 | * @param array $query_args Parameters supplied by the user. |
| 104 | * @param string $arg_name Target of the JOIN sql param. |
| 105 | */ |
| 106 | protected function add_from_sql_params( $query_args, $arg_name ) { |
| 107 | global $wpdb; |
| 108 | |
| 109 | if ( 'sku' !== $query_args['orderby'] ) { |
| 110 | return; |
| 111 | } |
| 112 | |
| 113 | $table_name = self::get_db_table_name(); |
| 114 | $join = "LEFT JOIN {$wpdb->postmeta} AS postmeta ON {$table_name}.variation_id = postmeta.post_id AND postmeta.meta_key = '_sku'"; |
| 115 | |
| 116 | if ( 'inner' === $arg_name ) { |
| 117 | $this->subquery->add_sql_clause( 'join', $join ); |
| 118 | } else { |
| 119 | $this->add_sql_clause( 'join', $join ); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Generate a subquery for order_item_id based on the attribute filters. |
| 125 | * |
| 126 | * @param array $query_args Query arguments supplied by the user. |
| 127 | * @return string |
| 128 | */ |
| 129 | protected function get_order_item_by_attribute_subquery( $query_args ) { |
| 130 | $order_product_lookup_table = self::get_db_table_name(); |
| 131 | $attribute_subqueries = $this->get_attribute_subqueries( $query_args ); |
| 132 | |
| 133 | if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) { |
| 134 | // Perform a subquery for DISTINCT order items that match our attribute filters. |
| 135 | $attr_subquery = new SqlQuery( $this->context . '_attribute_subquery' ); |
| 136 | $attr_subquery->add_sql_clause( 'select', "DISTINCT {$order_product_lookup_table}.order_item_id" ); |
| 137 | $attr_subquery->add_sql_clause( 'from', $order_product_lookup_table ); |
| 138 | |
| 139 | if ( $this->should_exclude_simple_products( $query_args ) ) { |
| 140 | $attr_subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" ); |
| 141 | } |
| 142 | |
| 143 | foreach ( $attribute_subqueries['join'] as $attribute_join ) { |
| 144 | $attr_subquery->add_sql_clause( 'join', $attribute_join ); |
| 145 | } |
| 146 | |
| 147 | $operator = $this->get_match_operator( $query_args ); |
| 148 | $attr_subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $attribute_subqueries['where'] ) . ')' ); |
| 149 | |
| 150 | return "AND {$order_product_lookup_table}.order_item_id IN ({$attr_subquery->get_query_statement()})"; |
| 151 | } |
| 152 | |
| 153 | return false; |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Updates the database query with parameters used for Products report: categories and order status. |
| 158 | * |
| 159 | * @param array $query_args Query arguments supplied by the user. |
| 160 | */ |
| 161 | protected function add_sql_query_params( $query_args ) { |
| 162 | global $wpdb; |
| 163 | $order_product_lookup_table = self::get_db_table_name(); |
| 164 | $order_stats_lookup_table = $wpdb->prefix . 'wc_order_stats'; |
| 165 | $order_item_meta_table = $wpdb->prefix . 'woocommerce_order_itemmeta'; |
| 166 | $where_subquery = array(); |
| 167 | |
| 168 | $this->add_time_period_sql_params( $query_args, $order_product_lookup_table ); |
| 169 | $this->get_limit_sql_params( $query_args ); |
| 170 | $this->add_order_by_sql_params( $query_args ); |
| 171 | |
| 172 | $included_variations = $this->get_included_variations( $query_args ); |
| 173 | if ( $included_variations > 0 ) { |
| 174 | $this->add_from_sql_params( $query_args, 'outer' ); |
| 175 | } else { |
| 176 | $this->add_from_sql_params( $query_args, 'inner' ); |
| 177 | } |
| 178 | |
| 179 | $included_products = $this->get_included_products( $query_args ); |
| 180 | if ( $included_products ) { |
| 181 | $this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" ); |
| 182 | } |
| 183 | |
| 184 | $excluded_products = $this->get_excluded_products( $query_args ); |
| 185 | if ( $excluded_products ) { |
| 186 | $this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id NOT IN ({$excluded_products})" ); |
| 187 | } |
| 188 | |
| 189 | if ( $included_variations ) { |
| 190 | $this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" ); |
| 191 | } elseif ( $this->should_exclude_simple_products( $query_args ) ) { |
| 192 | $this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" ); |
| 193 | } |
| 194 | |
| 195 | $order_status_filter = $this->get_status_subquery( $query_args ); |
| 196 | if ( $order_status_filter ) { |
| 197 | $this->subquery->add_sql_clause( 'join', "JOIN {$order_stats_lookup_table} ON {$order_product_lookup_table}.order_id = {$order_stats_lookup_table}.order_id" ); |
| 198 | $this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" ); |
| 199 | } |
| 200 | |
| 201 | $attribute_order_items_subquery = $this->get_order_item_by_attribute_subquery( $query_args ); |
| 202 | if ( $attribute_order_items_subquery ) { |
| 203 | // JOIN on product lookup if we haven't already. |
| 204 | if ( ! $order_status_filter ) { |
| 205 | $this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" ); |
| 206 | } |
| 207 | |
| 208 | // Add subquery for matching attributes to WHERE. |
| 209 | $this->subquery->add_sql_clause( 'where', $attribute_order_items_subquery ); |
| 210 | } |
| 211 | |
| 212 | if ( 0 < count( $where_subquery ) ) { |
| 213 | $operator = $this->get_match_operator( $query_args ); |
| 214 | $this->subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $where_subquery ) . ')' ); |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Maps ordering specified by the user to columns in the database/fields in the data. |
| 220 | * |
| 221 | * @override ReportsDataStore::normalize_order_by() |
| 222 | * |
| 223 | * @param string $order_by Sorting criterion. |
| 224 | * |
| 225 | * @return string |
| 226 | */ |
| 227 | protected function normalize_order_by( $order_by ) { |
| 228 | if ( 'date' === $order_by ) { |
| 229 | return self::get_db_table_name() . '.date_created'; |
| 230 | } |
| 231 | if ( 'sku' === $order_by ) { |
| 232 | return 'meta_value'; |
| 233 | } |
| 234 | |
| 235 | return $order_by; |
| 236 | } |
| 237 | |
| 238 | /** |
| 239 | * Enriches the product data with attributes specified by the extended_attributes. |
| 240 | * |
| 241 | * @param array $products_data Product data. |
| 242 | * @param array $query_args Query parameters. |
| 243 | */ |
| 244 | protected function include_extended_info( &$products_data, $query_args ) { |
| 245 | foreach ( $products_data as $key => $product_data ) { |
| 246 | $extended_info = new \ArrayObject(); |
| 247 | if ( $query_args['extended_info'] ) { |
| 248 | $extended_attributes = apply_filters( 'woocommerce_rest_reports_variations_extended_attributes', $this->extended_attributes, $product_data ); |
| 249 | $parent_product = wc_get_product( $product_data['product_id'] ); |
| 250 | $attributes = array(); |
| 251 | |
| 252 | // Base extended info off the parent variable product if the variation ID is 0. |
| 253 | // This is caused by simple products with prior sales being converted into variable products. |
| 254 | // See: https://github.com/woocommerce/woocommerce-admin/issues/2719. |
| 255 | $variation_id = (int) $product_data['variation_id']; |
| 256 | $variation_product = ( 0 === $variation_id ) ? $parent_product : wc_get_product( $variation_id ); |
| 257 | |
| 258 | // Fall back to the parent product if the variation can't be found. |
| 259 | $extended_attributes_product = is_a( $variation_product, 'WC_Product' ) ? $variation_product : $parent_product; |
| 260 | // If both product and variation is not found, set deleted to true. |
| 261 | if ( ! $extended_attributes_product ) { |
| 262 | $extended_info['deleted'] = true; |
| 263 | } |
| 264 | foreach ( $extended_attributes as $extended_attribute ) { |
| 265 | $function = 'get_' . $extended_attribute; |
| 266 | if ( is_callable( array( $extended_attributes_product, $function ) ) ) { |
| 267 | $value = $extended_attributes_product->{$function}(); |
| 268 | $extended_info[ $extended_attribute ] = $value; |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | // If this is a variation, add its attributes. |
| 273 | // NOTE: We don't fall back to the parent product here because it will include all possible attribute options. |
| 274 | if ( |
| 275 | 0 < $variation_id && |
| 276 | is_callable( array( $variation_product, 'get_variation_attributes' ) ) |
| 277 | ) { |
| 278 | $variation_attributes = $variation_product->get_variation_attributes(); |
| 279 | |
| 280 | foreach ( $variation_attributes as $attribute_name => $attribute ) { |
| 281 | $name = str_replace( 'attribute_', '', $attribute_name ); |
| 282 | $option_term = get_term_by( 'slug', $attribute, $name ); |
| 283 | $attributes[] = array( |
| 284 | 'id' => wc_attribute_taxonomy_id_by_name( $name ), |
| 285 | 'name' => str_replace( 'pa_', '', $name ), |
| 286 | 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, |
| 287 | ); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | $extended_info['attributes'] = $attributes; |
| 292 | |
| 293 | // If there is no set low_stock_amount, use the one in user settings. |
| 294 | if ( '' === $extended_info['low_stock_amount'] ) { |
| 295 | $extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); |
| 296 | } |
| 297 | $extended_info = $this->cast_numbers( $extended_info ); |
| 298 | } |
| 299 | $products_data[ $key ]['extended_info'] = $extended_info; |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | /** |
| 304 | * Returns if simple products should be excluded from the report. |
| 305 | * |
| 306 | * @internal |
| 307 | * |
| 308 | * @param array $query_args Query parameters. |
| 309 | * |
| 310 | * @return boolean |
| 311 | */ |
| 312 | protected function should_exclude_simple_products( array $query_args ) { |
| 313 | return apply_filters( 'experimental_woocommerce_analytics_variations_should_exclude_simple_products', true, $query_args ); |
| 314 | } |
| 315 | |
| 316 | /** |
| 317 | * Fill missing extended_info.name for the deleted products. |
| 318 | * |
| 319 | * @param array $products Product data. |
| 320 | */ |
| 321 | protected function fill_deleted_product_name( array &$products ) { |
| 322 | global $wpdb; |
| 323 | $product_variation_ids = array(); |
| 324 | // Find products with missing extended_info.name. |
| 325 | foreach ( $products as $key => $product ) { |
| 326 | if ( ! isset( $product['extended_info']['name'] ) ) { |
| 327 | $product_variation_ids[ $key ] = array( |
| 328 | 'product_id' => $product['product_id'], |
| 329 | 'variation_id' => $product['variation_id'], |
| 330 | ); |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | if ( ! count( $product_variation_ids ) ) { |
| 335 | return; |
| 336 | } |
| 337 | |
| 338 | $where_clauses = implode( |
| 339 | ' or ', |
| 340 | array_map( |
| 341 | function ( $ids ) { |
| 342 | return "( |
| 343 | product_lookup.product_id = {$ids['product_id']} |
| 344 | and |
| 345 | product_lookup.variation_id = {$ids['variation_id']} |
| 346 | )"; |
| 347 | }, |
| 348 | $product_variation_ids |
| 349 | ) |
| 350 | ); |
| 351 | |
| 352 | $query = " |
| 353 | select |
| 354 | product_lookup.product_id, |
| 355 | product_lookup.variation_id, |
| 356 | order_items.order_item_name |
| 357 | from |
| 358 | {$wpdb->prefix}wc_order_product_lookup as product_lookup |
| 359 | left join {$wpdb->prefix}woocommerce_order_items as order_items |
| 360 | on product_lookup.order_item_id = order_items.order_item_id |
| 361 | where |
| 362 | {$where_clauses} |
| 363 | group by |
| 364 | product_lookup.product_id, |
| 365 | product_lookup.variation_id, |
| 366 | order_items.order_item_name |
| 367 | "; |
| 368 | |
| 369 | // phpcs:ignore |
| 370 | $results = $wpdb->get_results( $query ); |
| 371 | $index = array(); |
| 372 | foreach ( $results as $result ) { |
| 373 | $index[ $result->product_id . '_' . $result->variation_id ] = $result->order_item_name; |
| 374 | } |
| 375 | |
| 376 | foreach ( $product_variation_ids as $product_key => $ids ) { |
| 377 | $product = $products[ $product_key ]; |
| 378 | $index_key = $product['product_id'] . '_' . $product['variation_id']; |
| 379 | if ( isset( $index[ $index_key ] ) ) { |
| 380 | $products[ $product_key ]['extended_info']['name'] = $index[ $index_key ]; |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | /** |
| 386 | * Get the default query arguments to be used by get_data(). |
| 387 | * These defaults are only partially applied when used via REST API, as that has its own defaults. |
| 388 | * |
| 389 | * @override ReportsDataStore::get_default_query_vars() |
| 390 | * |
| 391 | * @return array Query parameters. |
| 392 | */ |
| 393 | public function get_default_query_vars() { |
| 394 | $defaults = parent::get_default_query_vars(); |
| 395 | $defaults['product_includes'] = array(); |
| 396 | $defaults['variation_includes'] = array(); |
| 397 | $defaults['extended_info'] = false; |
| 398 | |
| 399 | return $defaults; |
| 400 | } |
| 401 | |
| 402 | /** |
| 403 | * Returns the report data based on normalized parameters. |
| 404 | * Will be called by `get_data` if there is no data in cache. |
| 405 | * |
| 406 | * @override ReportsDataStore::get_noncached_data() |
| 407 | * |
| 408 | * @see get_data |
| 409 | * @param array $query_args Query parameters. |
| 410 | * @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error. |
| 411 | */ |
| 412 | public function get_noncached_data( $query_args ) { |
| 413 | global $wpdb; |
| 414 | |
| 415 | $table_name = self::get_db_table_name(); |
| 416 | |
| 417 | $this->initialize_queries(); |
| 418 | |
| 419 | $data = (object) array( |
| 420 | 'data' => array(), |
| 421 | 'total' => 0, |
| 422 | 'pages' => 0, |
| 423 | 'page_no' => 0, |
| 424 | ); |
| 425 | |
| 426 | $selections = $this->selected_columns( $query_args ); |
| 427 | $included_variations = |
| 428 | ( isset( $query_args['variation_includes'] ) && is_array( $query_args['variation_includes'] ) ) |
| 429 | ? $query_args['variation_includes'] |
| 430 | : array(); |
| 431 | $params = $this->get_limit_params( $query_args ); |
| 432 | $this->add_sql_query_params( $query_args ); |
| 433 | |
| 434 | if ( count( $included_variations ) > 0 ) { |
| 435 | $total_results = count( $included_variations ); |
| 436 | $total_pages = (int) ceil( $total_results / $params['per_page'] ); |
| 437 | |
| 438 | $this->subquery->clear_sql_clause( 'select' ); |
| 439 | $this->subquery->add_sql_clause( 'select', $selections ); |
| 440 | |
| 441 | if ( 'date' === $query_args['orderby'] ) { |
| 442 | $this->subquery->add_sql_clause( 'select', ", {$table_name}.date_created" ); |
| 443 | } |
| 444 | |
| 445 | $fields = $this->get_fields( $query_args ); |
| 446 | $join_selections = $this->format_join_selections( $fields, array( 'variation_id' ) ); |
| 447 | $ids_table = $this->get_ids_table( $included_variations, 'variation_id' ); |
| 448 | |
| 449 | $this->add_sql_clause( 'select', $join_selections ); |
| 450 | $this->add_sql_clause( 'from', '(' ); |
| 451 | $this->add_sql_clause( 'from', $this->subquery->get_query_statement() ); |
| 452 | $this->add_sql_clause( 'from', ") AS {$table_name}" ); |
| 453 | $this->add_sql_clause( |
| 454 | 'right_join', |
| 455 | "RIGHT JOIN ( {$ids_table} ) AS default_results |
| 456 | ON default_results.variation_id = {$table_name}.variation_id" |
| 457 | ); |
| 458 | |
| 459 | $variations_query = $this->get_query_statement(); |
| 460 | } else { |
| 461 | |
| 462 | $this->subquery->clear_sql_clause( 'select' ); |
| 463 | $this->subquery->add_sql_clause( 'select', $selections ); |
| 464 | |
| 465 | /** |
| 466 | * Experimental: Filter the Variations SQL query allowing extensions to add additional SQL clauses. |
| 467 | * |
| 468 | * @since 7.4.0 |
| 469 | * @param array $query_args Query parameters. |
| 470 | * @param SqlQuery $subquery Variations query class. |
| 471 | */ |
| 472 | apply_filters( 'experimental_woocommerce_analytics_variations_additional_clauses', $query_args, $this->subquery ); |
| 473 | |
| 474 | /* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */ |
| 475 | $db_records_count = (int) $wpdb->get_var( |
| 476 | "SELECT COUNT(*) FROM ( |
| 477 | {$this->subquery->get_query_statement()} |
| 478 | ) AS tt" |
| 479 | ); |
| 480 | /* phpcs:enable */ |
| 481 | |
| 482 | $total_results = $db_records_count; |
| 483 | $total_pages = (int) ceil( $db_records_count / $params['per_page'] ); |
| 484 | |
| 485 | if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) { |
| 486 | return $data; |
| 487 | } |
| 488 | |
| 489 | if ( in_array( $query_args['orderby'], array( 'items_sold', 'net_revenue', 'orders_count' ), true ) ) { |
| 490 | $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) . ', product_id, variation_id' ); |
| 491 | } else { |
| 492 | $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) ); |
| 493 | } |
| 494 | $this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) ); |
| 495 | $variations_query = $this->subquery->get_query_statement(); |
| 496 | } |
| 497 | |
| 498 | /* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */ |
| 499 | $product_data = $wpdb->get_results( |
| 500 | $variations_query, |
| 501 | ARRAY_A |
| 502 | ); |
| 503 | /* phpcs:enable */ |
| 504 | |
| 505 | if ( null === $product_data ) { |
| 506 | return $data; |
| 507 | } |
| 508 | |
| 509 | $this->include_extended_info( $product_data, $query_args ); |
| 510 | |
| 511 | if ( $query_args['extended_info'] ) { |
| 512 | $this->fill_deleted_product_name( $product_data ); |
| 513 | } |
| 514 | |
| 515 | $product_data = array_map( array( $this, 'cast_numbers' ), $product_data ); |
| 516 | $data = (object) array( |
| 517 | 'data' => $product_data, |
| 518 | 'total' => $total_results, |
| 519 | 'pages' => $total_pages, |
| 520 | 'page_no' => (int) $query_args['page'], |
| 521 | ); |
| 522 | |
| 523 | return $data; |
| 524 | } |
| 525 | |
| 526 | /** |
| 527 | * Initialize query objects. |
| 528 | */ |
| 529 | protected function initialize_queries() { |
| 530 | $this->clear_all_clauses(); |
| 531 | $this->subquery = new SqlQuery( $this->context . '_subquery' ); |
| 532 | $this->subquery->add_sql_clause( 'select', 'product_id' ); |
| 533 | $this->subquery->add_sql_clause( 'from', self::get_db_table_name() ); |
| 534 | $this->subquery->add_sql_clause( 'group_by', 'product_id, variation_id' ); |
| 535 | } |
| 536 | } |
| 537 |