PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
← All changes | includes/Sync/Digest_Index.php +179 -58 1.10.21.10.18 View file →
@@ -40,8 +40,17 @@
40 40 * Legacy baseline of postmeta keys covered by the product/variation digest. Must include every key the
41 41 * sql-bypass fixture mutates (_price and _regular_price today — see
42 42 * class-fixtures-controller.php sql_bypass()) plus the keys a
43 43 * hook-bypassing import/inventory tool plausibly touches.
44 + *
45 + * Changing the MEMBERSHIP of this set moves two things, with two different
46 + * levers (ADR 0036): the STORED digests rebuild automatically — this set
47 + * feeds {@see digest_formula_fingerprint}, whose move schedules the guarded
48 + * product-digest rebuild — and the digests CLIENTS hold go stale, which is
49 + * what bumping Config_Fingerprint::PAYLOAD_CONTRACT_VERSION for BOTH owners
50 + * (products AND variations — the set is shared) in the same commit repairs.
51 + * Reordering without a membership change is a no-op everywhere (SQL sorts).
52 + * The fingerprint tests pin the semantic set.
44 53 */
45 54 public const DIGESTED_META_KEYS = array( '_global_unique_id', '_price', '_regular_price', '_sale_price', '_sku', '_stock', '_stock_status' );
46 55
47 56 /**
@@ -58,8 +67,17 @@
58 67 * meta joins them at runtime in {@see customer_digest_select_sql} (the prefix is per-site, so it
59 68 * cannot live in a const): roles are part of the served record under #1379, and a hookless
60 69 * capabilities write (direct update_user_meta/SQL/import) must drift the digest so the
61 70 * integrity scan can repair the stale role — no role/profile hook fires for those writes.
71 + *
72 + * Changing the MEMBERSHIP of this set (ADR 0036): bump
73 + * Config_Fingerprint::PAYLOAD_CONTRACT_VERSION for `customers` in the same
74 + * commit so clients re-pull — and know that the STORED customer digests have
75 + * NO automatic rebuild trigger today: {@see digest_formula_fingerprint} folds
76 + * the product keys only, so the scan reports store-wide false customer drift
77 + * until a manual rebuild. Extending the rebuild trigger to this set is
78 + * #1756 phase 2/3 work; until it lands, a change here also needs a
79 + * deliberate rebuild plan. The fingerprint tests pin the semantic set.
62 80 */
63 81 public const CUSTOMER_DIGESTED_META_KEYS = array( 'first_name', 'last_name', 'billing_email', 'billing_phone' );
64 82
65 83 /**
@@ -65,8 +83,14 @@
65 83 /**
66 84 * Order postmeta folded into the digest under the CPT (legacy) storage path (ADR 0015, Leg-3 phase 7).
67 85 * Under HPOS these live as wc_orders COLUMNS (total_amount, customer_id) so no meta join is needed;
68 86 * this allowlist only applies to the wp_posts fallback. Kept minimal — existence/identity signal.
87 + *
88 + * Changing the MEMBERSHIP of this set (ADR 0036): bump
89 + * Config_Fingerprint::PAYLOAD_CONTRACT_VERSION for `orders` in the same
90 + * commit. Like the customer set, it has NO automatic stored-digest rebuild
91 + * trigger today ({@see digest_formula_fingerprint} folds product keys only) —
92 + * #1756 phase 2/3 owns that. The fingerprint tests pin the semantic set.
69 93 */
70 94 public const ORDER_DIGESTED_META_KEYS = array( '_order_total', '_customer_user' );
71 95
72 96 /**
@@ -224,10 +248,9 @@
224 248 // backstop, and CRC32 is linear so structured bulk edits correlate collisions).
225 249 . ')),1,16),16,10) AS UNSIGNED) AS crc'
226 250 . " FROM {$wpdb->posts} p"
227 251 . " LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key IN {$meta_keys_sql}"
228 - . ' WHERE p.post_type IN ' . self::PRODUCT_POST_TYPES_SQL
229 - . ' AND p.post_status NOT IN ' . self::EXCLUDED_POST_STATUSES_SQL
252 + . ' WHERE ' . $this->live_product_predicate_sql( 'p' )
230 253 . ( '' === $where_sql ? '' : ' AND ' . $where_sql )
231 254 . ' GROUP BY p.ID';
232 255 }
233 256
@@ -290,9 +313,9 @@
290 313 . " COALESCE(o.total_amount,''),"
291 314 . ' COALESCE(o.customer_id,0),'
292 315 . " COALESCE(o.date_updated_gmt,''))),1,16),16,10) AS UNSIGNED) AS crc"
293 316 . " FROM {$orders_table} o"
294 - . " WHERE o.type = 'shop_order' AND o.status NOT IN ('trash','auto-draft')"
317 + . ' WHERE ' . $this->live_order_predicate_sql( 'o' )
295 318 . $condition;
296 319 }
297 320
298 321 $meta_keys_sql = "('" . implode( "','", self::ORDER_DIGESTED_META_KEYS ) . "')";
@@ -304,9 +327,9 @@
304 327 . " COALESCE(p.post_modified_gmt,''),"
305 328 . " COALESCE(GROUP_CONCAT(CONCAT(pm.meta_key,'=',COALESCE(pm.meta_value,'')) ORDER BY pm.meta_key ASC, pm.meta_id ASC SEPARATOR '|'),''))),1,16),16,10) AS UNSIGNED) AS crc"
306 329 . " FROM {$wpdb->posts} p"
307 330 . " LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key IN {$meta_keys_sql}"
308 - . " WHERE p.post_type = 'shop_order' AND p.post_status NOT IN " . self::EXCLUDED_POST_STATUSES_SQL
331 + . ' WHERE ' . $this->live_order_predicate_sql( 'p' )
309 332 . $condition
310 333 . ' GROUP BY p.ID';
311 334 }
312 335
@@ -329,8 +352,13 @@
329 352 * @return array<int, string>
330 353 */
331 354 public function read_digests( string $collection, array $ids ): array {
332 355 global $wpdb;
356 + // Order/customer upserts are coalesced per request and land at shutdown
357 + // (see Integrity_Digest::$pending_digests). A read inside the same
358 + // request — the pull lane stamping `_rxdb_digest` after a write — must
359 + // see the settled digest, so land what is owed before reading.
360 + Integrity_Digest::flush_pending_digests();
333 361 $object_types = self::digest_object_types( $collection );
334 362 if ( array() === $object_types ) {
335 363 return array();
336 364 }
@@ -390,15 +418,15 @@
390 418 $live_rows = isset( $row['digest']['live_rows'] ) ? (string) $row['digest']['live_rows'] : '';
391 419 // Products are the one collection whose servability is NARROWER than a live
392 420 // row: POS visibility and the readable-catalog scope both apply, so the
393 421 // richer reader owns them. Every other id-space is exactly its live row.
394 - $products = 'products' === $collection;
395 - if ( ! $products && ( '' === $live_rows || ! method_exists( $this, $live_rows ) ) ) {
422 + $servable = $row['digest']['servable'] ?? null;
423 + if ( null === $servable && ( '' === $live_rows || ! method_exists( $this, $live_rows ) ) ) {
396 424 return $ids;
397 425 }
398 426 $wpdb->last_error = '';
399 - if ( $products ) {
400 - $servable_ids = $this->servable_product_ids( $ids, true );
427 + if ( null !== $servable ) {
428 + $servable_ids = $this->$servable( $ids, true );
401 429 } else {
402 430 $predicate = (string) \call_user_func( array( $this, $live_rows ), 'requested.id' );
403 431 $requested = implode( ' UNION ALL ', array_fill( 0, \count( $ids ), 'SELECT %d AS id' ) );
404 432 $servable_ids = $wpdb->get_col(
@@ -453,10 +481,9 @@
453 481 public function live_row_exists_sql( string $id_expr ): string {
454 482 global $wpdb;
455 483
456 484 return "EXISTS (SELECT 1 FROM {$wpdb->posts} lp WHERE lp.ID = {$id_expr}"
457 - . ' AND lp.post_type IN ' . self::PRODUCT_POST_TYPES_SQL
458 - . ' AND lp.post_status NOT IN ' . self::EXCLUDED_POST_STATUSES_SQL . ')';
485 + . ' AND ' . $this->live_product_predicate_sql( 'lp' ) . ')';
459 486 }
460 487
461 488 /**
462 489 * Customer analogue of {@see live_row_exists_sql}: the id still names any WordPress user (ADR 0015).
@@ -478,12 +505,12 @@
478 505 global $wpdb;
479 506 if ( $this->orders_are_hpos() ) {
480 507 $orders_table = $wpdb->prefix . 'wc_orders';
481 508 return "EXISTS (SELECT 1 FROM {$orders_table} lo WHERE lo.id = {$id_expr}"
482 - . " AND lo.type = 'shop_order' AND lo.status NOT IN ('trash','auto-draft'))";
509 + . ' AND ' . $this->live_order_predicate_sql( 'lo' ) . ')';
483 510 }
484 511 return "EXISTS (SELECT 1 FROM {$wpdb->posts} lp WHERE lp.ID = {$id_expr}"
485 - . " AND lp.post_type = 'shop_order' AND lp.post_status NOT IN " . self::EXCLUDED_POST_STATUSES_SQL . ')';
512 + . ' AND ' . $this->live_order_predicate_sql( 'lp' ) . ')';
486 513 }
487 514
488 515 /**
489 516 * Stored-vs-current bucket aggregate over one id window (the integrity scan).
@@ -509,21 +536,18 @@
509 536 global $wpdb;
510 537 $bucket_size = max( 1, (int) ( $range['bucket_size'] ?? 1 ) );
511 538 $window_start = max( 0, (int) ( $range['start'] ?? 0 ) );
512 539 $window_end = max( 0, (int) ( $range['end'] ?? 0 ) );
513 - $publish = 'products' === $collection && 'publish' === ( $filters['status'] ?? '' );
514 - $object_types = self::OBJECT_TYPES_SQL;
515 - $current_sql = $this->row_digest_select_sql( 'p.ID >= %d AND p.ID < %d' );
516 - $max_sql = $this->row_digest_select_sql();
517 - if ( 'customers' === $collection ) {
518 - $object_types = "('customer')";
519 - $current_sql = $this->customer_digest_select_sql( 'u.ID >= %d AND u.ID < %d' );
520 - $max_sql = $this->customer_digest_select_sql();
521 - } elseif ( 'orders' === $collection ) {
522 - $object_types = "('order')";
523 - $current_sql = $this->order_digest_select_sql( '{id} >= %d AND {id} < %d' );
524 - $max_sql = $this->order_digest_select_sql();
540 + $digest = Collections::row( $collection )['digest'] ?? null;
541 + if ( null === $digest ) {
542 + return array(
543 + 'buckets' => array(),
544 + 'max_id' => 0,
545 + );
525 546 }
547 + $publish = isset( Collections::row( $collection )['digest']['published_ids'] ) && 'publish' === ( $filters['status'] ?? '' );
548 + $object_types = "('" . implode( "','", $digest['object_types'] ) . "')";
549 + $current_sql = $this->{$digest['select']}( $digest['id_column'] . ' >= %d AND ' . $digest['id_column'] . ' < %d' );
526 550 $current_scope = $publish ? $this->product_servable_predicate_sql( 't.id', true ) : array(
527 551 'sql' => '',
528 552 'args' => array(),
529 553 );
@@ -530,10 +554,10 @@
530 554 $stored_scope = $publish ? $this->product_servable_predicate_sql( 'd.object_id', true ) : array(
531 555 'sql' => '',
532 556 'args' => array(),
533 557 );
534 - $current_join = $publish ? " INNER JOIN {$wpdb->posts} catalog_post ON catalog_post.ID = t.id LEFT JOIN {$wpdb->posts} parent_product ON parent_product.ID = catalog_post.post_parent AND catalog_post.post_type = 'product_variation'" : '';
535 - $stored_join = $publish ? " INNER JOIN {$wpdb->posts} catalog_post ON catalog_post.ID = d.object_id LEFT JOIN {$wpdb->posts} parent_product ON parent_product.ID = catalog_post.post_parent AND catalog_post.post_type = 'product_variation'" : '';
558 + $current_join = $publish ? $this->servable_product_join_sql( 't.id' ) : '';
559 + $stored_join = $publish ? $this->servable_product_join_sql( 'd.object_id' ) : '';
536 560
537 561 // Current side: one SQL pass — per-row canonical digests aggregated
538 562 // per bucket inside the DB engine. Raw rows are digested for
539 563 // DETECTION only; hydration goes through filtered REST (ADR 0003).
@@ -597,27 +621,18 @@
597 621 'match' => $stored_count === $current_count && $stored_digest === $current_digest,
598 622 );
599 623 }
600 624
601 - $max_query =
602 - 'SELECT GREATEST('
603 - . "COALESCE((SELECT MAX(ID) FROM {$wpdb->posts} WHERE post_type IN " . self::PRODUCT_POST_TYPES_SQL
604 - . ' AND post_status NOT IN ' . self::EXCLUDED_POST_STATUSES_SQL . '), 0),'
605 - . ' COALESCE((SELECT MAX(object_id) FROM ' . $this->table_name()
606 - . ' WHERE object_type IN ' . self::OBJECT_TYPES_SQL . '), 0))';
607 - $max_args = array();
608 - if ( 'products' !== $collection || $publish ) {
609 - $live_scope = $publish ? $this->product_servable_predicate_sql( 'live.id', true ) : array(
610 - 'sql' => '',
611 - 'args' => array(),
612 - );
613 - $live_join = $publish ? " INNER JOIN {$wpdb->posts} catalog_post ON catalog_post.ID = live.id LEFT JOIN {$wpdb->posts} parent_product ON parent_product.ID = catalog_post.post_parent AND catalog_post.post_type = 'product_variation'" : '';
614 - $max_query = 'SELECT GREATEST(COALESCE((SELECT MAX(live.id) FROM (' . $max_sql . ') live' . $live_join . ( '' === $live_scope['sql'] ? '' : ' WHERE ' . $live_scope['sql'] ) . '), 0),'
615 - . ' COALESCE((SELECT MAX(d.object_id) FROM ' . $this->table_name() . ' d'
616 - . ' WHERE d.object_type IN ' . $object_types . '), 0))';
617 - $max_args = $live_scope['args'];
618 - }
619 - $max_id = (int) $wpdb->get_var( empty( $max_args ) ? $max_query : $wpdb->prepare( $max_query, ...$max_args ) );
625 + // Completion id: the larger of the last LIVE id under the collection's own
626 + // servable predicate and the last STORED id. The live side is MAX(id)
627 + // straight off the base table — wrapping the un-windowed per-row digest
628 + // SELECT as a derived table just to take its max digested the whole
629 + // collection on every scan page (0.4–3 s on real stores; #1805, ADR 0038).
630 + $live_max = $this->live_max_id_sql( $collection, $publish );
631 + $max_query = 'SELECT GREATEST(COALESCE((' . $live_max['sql'] . '), 0),'
632 + . ' COALESCE((SELECT MAX(d.object_id) FROM ' . $this->table_name() . ' d'
633 + . ' WHERE d.object_type IN ' . $object_types . '), 0))';
634 + $max_id = (int) $wpdb->get_var( empty( $live_max['args'] ) ? $max_query : $wpdb->prepare( $max_query, ...$live_max['args'] ) );
620 635
621 636 return array(
622 637 'buckets' => $buckets,
623 638 'max_id' => $max_id,
@@ -640,8 +655,9 @@
640 655 public function bucket_drift( array $range ): array {
641 656 global $wpdb;
642 657 $range_start = max( 0, (int) ( $range['start'] ?? 0 ) );
643 658 $range_end = max( 0, (int) ( $range['end'] ?? 0 ) );
659 + $digest = Collections::row( 'products' )['digest'];
644 660 $table = $this->table_name();
645 661
646 662 // Same-formula invariant: the current side must digest identically to the stored side.
647 663 $this->raise_group_concat_max_len();
@@ -649,17 +665,17 @@
649 665 $wpdb->prepare(
650 666 'SELECT cur.id AS id,'
651 667 . " CASE WHEN d.digest IS NULL THEN 'missing_stored' ELSE 'changed' END AS status,"
652 668 . ' d.digest AS stored_digest, cur.crc AS current_digest, cur.object_type AS object_type'
653 - . ' FROM (' . $this->row_digest_select_sql( 'p.ID >= %d AND p.ID < %d' ) . ') cur'
669 + . ' FROM (' . $this->{$digest['select']}( $digest['id_column'] . ' >= %d AND ' . $digest['id_column'] . ' < %d' ) . ') cur'
654 670 . " LEFT JOIN {$table} d ON d.object_id = cur.id AND d.object_type = cur.object_type"
655 671 . ' WHERE d.digest IS NULL OR d.digest <> cur.crc'
656 672 . ' UNION ALL'
657 673 . " SELECT d.object_id AS id, 'deleted' AS status, d.digest AS stored_digest, NULL AS current_digest, d.object_type AS object_type"
658 674 . " FROM {$table} d"
659 - . ' WHERE d.object_type IN ' . self::OBJECT_TYPES_SQL
675 + . ' WHERE d.object_type IN ' . "('" . implode( "','", $digest['object_types'] ) . "')"
660 676 . ' AND d.object_id >= %d AND d.object_id < %d'
661 - . ' AND NOT ' . $this->live_row_exists_sql( 'd.object_id' )
677 + . ' AND NOT ' . $this->{$digest['live_rows']}( 'd.object_id' )
662 678 . ' ORDER BY id ASC',
663 679 $range_start,
664 680 $range_end,
665 681 $range_start,
@@ -710,15 +726,14 @@
710 726
711 727 $servable_join = '';
712 728 $servable_filter = '';
713 729 $servable_args = array();
714 - if ( 'customers' === $collection ) {
715 - $inner_sql = $this->customer_digest_select_sql( 'u.ID >= %d AND u.ID < %d' );
716 - } elseif ( 'orders' === $collection ) {
717 - // Orders bucket over their own id-space (HPOS o.id / CPT p.ID) via the {id} placeholder.
718 - $inner_sql = $this->order_digest_select_sql( '{id} >= %d AND {id} < %d' );
719 - } else {
720 - $inner_sql = $this->row_digest_select_sql( 'p.ID >= %d AND p.ID < %d' );
730 + $digest = Collections::row( $collection )['digest'] ?? null;
731 + if ( null === $digest ) {
732 + return array();
733 + }
734 + $inner_sql = $this->{$digest['select']}( $digest['id_column'] . ' >= %d AND ' . $digest['id_column'] . ' < %d' );
735 + if ( isset( $digest['servable'] ) ) {
721 736 $servable_join = " INNER JOIN {$wpdb->posts} catalog_post ON catalog_post.ID = cur.id";
722 737 if ( 'publish' === ( $filters['status'] ?? '' ) ) {
723 738 $servable_join .= " LEFT JOIN {$wpdb->posts} parent_product ON parent_product.ID = catalog_post.post_parent"
724 739 . " AND catalog_post.post_type = 'product_variation'";
@@ -814,11 +829,10 @@
814 829 public function needs_product_rebuild(): bool {
815 830 global $wpdb;
816 831
817 832 return (bool) $wpdb->get_var(
818 - 'SELECT EXISTS (SELECT 1 FROM ' . $wpdb->posts
819 - . ' WHERE post_type IN ' . self::PRODUCT_POST_TYPES_SQL
820 - . ' AND post_status NOT IN ' . self::EXCLUDED_POST_STATUSES_SQL . ' LIMIT 1)'
833 + 'SELECT EXISTS (SELECT 1 FROM ' . $wpdb->posts . ' p'
834 + . ' WHERE ' . $this->live_product_predicate_sql( 'p' ) . ' LIMIT 1)'
821 835 . ' AND NOT EXISTS (SELECT 1 FROM ' . $this->table_name()
822 836 . ' WHERE object_type IN ' . self::OBJECT_TYPES_SQL . ' LIMIT 1)'
823 837 );
824 838 }
@@ -830,8 +844,115 @@
830 844 private function published_product_predicate_sql( string $post_alias, string $parent_alias ): string {
831 845 return "(({$post_alias}.post_type = 'product' AND {$post_alias}.post_status = 'publish')"
832 846 . " OR ({$post_alias}.post_type = 'product_variation' AND {$parent_alias}.post_type = 'product'"
833 847 . " AND {$parent_alias}.post_status = 'publish'))";
848 + }
849 +
850 + /**
851 + * A LIVE product-space row over a `wp_posts` alias: a product or variation that
852 + * is not trashed/auto-draft. ONE spelling for the digest SELECT, the live-row
853 + * probes, the rebuild guard and the completion id — the scan is only sound
854 + * while every side agrees on what "live" means.
855 + */
856 + private function live_product_predicate_sql( string $alias ): string {
857 + return "{$alias}.post_type IN " . self::PRODUCT_POST_TYPES_SQL
858 + . " AND {$alias}.post_status NOT IN " . self::EXCLUDED_POST_STATUSES_SQL;
859 + }
860 +
861 + /**
862 + * A LIVE order over the active order store's alias — `wc_orders` under HPOS,
863 + * `wp_posts` under legacy CPT — with the same one-spelling rule as
864 + * {@see live_product_predicate_sql}.
865 + */
866 + private function live_order_predicate_sql( string $alias ): string {
867 + if ( $this->orders_are_hpos() ) {
868 + return "{$alias}.type = 'shop_order' AND {$alias}.status NOT IN " . self::EXCLUDED_POST_STATUSES_SQL;
869 + }
870 +
871 + return "{$alias}.post_type = 'shop_order' AND {$alias}.post_status NOT IN " . self::EXCLUDED_POST_STATUSES_SQL;
872 + }
873 +
874 + /**
875 + * The joins {@see product_servable_predicate_sql} reads through: the post row
876 + * behind `$id_expr` as `catalog_post`, and its parent as `parent_product` when
877 + * it is a variation. One spelling for every side of the scan.
878 + */
879 + private function servable_product_join_sql( string $id_expr ): string {
880 + global $wpdb;
881 +
882 + return " INNER JOIN {$wpdb->posts} catalog_post ON catalog_post.ID = {$id_expr}" . $this->parent_product_join_sql();
883 + }
884 +
885 + /**
886 + * `catalog_post`'s parent as `parent_product` when it is a variation (NULL otherwise).
887 + */
888 + private function parent_product_join_sql(): string {
889 + global $wpdb;
890 +
891 + return " LEFT JOIN {$wpdb->posts} parent_product ON parent_product.ID = catalog_post.post_parent AND catalog_post.post_type = 'product_variation'";
892 + }
893 +
894 + /**
895 + * `MAX(id)` of a collection's LIVE rows under the same predicate its digest
896 + * SELECT uses — off the base table, never through a digested row (#1805).
897 + *
898 + * Customers are every `wp_users` row (#1379), so this is the primary key's end.
899 + * Orders and products carry a type/status predicate, so this is one index pass
900 + * over the live rows of that type — the honest floor, since no index ends on
901 + * the id under a status filter. Under the published product scope the servable
902 + * predicate (published, or a variation of a published parent, and not POS-hidden)
903 + * applies to the post row itself, exactly as the windowed sides apply it.
904 + *
905 + * @return array{sql: string, args: array<int, int>}
906 + */
907 + private function live_max_id_sql( string $collection, bool $publish ): array {
908 + $digest = Collections::row( $collection )['digest'] ?? null;
909 + if ( null === $digest ) {
910 + return array(
911 + 'sql' => 'SELECT NULL WHERE 1 = 0',
912 + 'args' => array(),
913 + );
914 + }
915 + return $this->{$digest['live_max']}( $publish );
916 + }
917 +
918 + private function customer_live_max_id_sql(): array {
919 + global $wpdb;
920 + return array(
921 + 'sql' => "SELECT MAX(u.ID) FROM {$wpdb->users} u",
922 + 'args' => array(),
923 + );
924 + }
925 +
926 + private function order_live_max_id_sql(): array {
927 + global $wpdb;
928 + if ( $this->orders_are_hpos() ) {
929 + $orders_table = $wpdb->prefix . 'wc_orders';
930 + return array(
931 + 'sql' => "SELECT MAX(o.id) FROM {$orders_table} o WHERE " . $this->live_order_predicate_sql( 'o' ),
932 + 'args' => array(),
933 + );
934 + }
935 + return array(
936 + 'sql' => "SELECT MAX(p.ID) FROM {$wpdb->posts} p WHERE " . $this->live_order_predicate_sql( 'p' ),
937 + 'args' => array(),
938 + );
939 + }
940 +
941 + private function product_live_max_id_sql( bool $publish ): array {
942 + global $wpdb;
943 + if ( ! $publish ) {
944 + return array(
945 + 'sql' => "SELECT MAX(p.ID) FROM {$wpdb->posts} p WHERE " . $this->live_product_predicate_sql( 'p' ),
946 + 'args' => array(),
947 + );
948 + }
949 + $scope = $this->product_servable_predicate_sql( 'catalog_post.ID', true );
950 + return array(
951 + 'sql' => "SELECT MAX(catalog_post.ID) FROM {$wpdb->posts} catalog_post" . $this->parent_product_join_sql()
952 + . ' WHERE ' . $scope['sql'],
953 + 'args' => $scope['args'],
954 + );
834 955 }
835 956
836 957 private function product_servable_predicate_sql( string $id_expr, bool $publish ): array {
837 958 $predicates = array(