get( 'mapsemble_entropy' ); if ( '' === $seed ) { return $orderby; } // The coordinate aliases come from WP_Meta_Query itself, via the // named clauses the feed registers — no user input reaches the SQL. $clauses = isset( $query->meta_query ) && is_object( $query->meta_query ) ? $query->meta_query->get_clauses() : array(); $lat = (string) ( $clauses['mapsemble_lat']['alias'] ?? '' ); $lng = (string) ( $clauses['mapsemble_lng']['alias'] ?? '' ); /** * Filters whether the database can run the entropy window function. * * @param bool $supported True on MySQL 8+ / MariaDB 10.2+. */ $supported = (bool) apply_filters( 'mapsemble_entropy_supported', $this->server_supports() ); if ( '' === $lat || '' === $lng || ! $supported ) { // Same spirit as Drupal's random fallback, but seeded by the map // so paging across offset requests stays coherent per map. return sprintf( 'RAND(%u)', crc32( $seed ) ); } return self::entropy_sql( $lat, $lng ); } /** * Builds the entropy ORDER BY for two postmeta aliases. * * Every joined-column reference is wrapped in MIN(): WP_Query adds * `GROUP BY posts.ID` whenever postmeta joins, and only_full_group_by * rejects bare joined columns inside window functions. Lat/lng meta is * single-valued, so MIN() is the value. * * @param string $lat_alias Alias of the latitude meta join. * @param string $lng_alias Alias of the longitude meta join. */ public static function entropy_sql( string $lat_alias, string $lng_alias ): string { $lat_value = "MIN(CAST({$lat_alias}.meta_value AS DECIMAL(9,6)))"; $lng_value = "MIN(CAST({$lng_alias}.meta_value AS DECIMAL(9,6)))"; $cell = "CONCAT(FLOOR(({$lat_value} + 90) / 5.625), '_', FLOOR(({$lng_value} + 180) / 11.25))"; $tie = "MD5(CONCAT(MIN({$lat_alias}.meta_value), ',', MIN({$lng_alias}.meta_value)))"; return "ROW_NUMBER() OVER (PARTITION BY {$cell} ORDER BY {$tie}) ASC, MD5({$cell}) ASC"; } /** * Whether this database serves window functions. */ private function server_supports(): bool { global $wpdb; return self::version_supports( (string) $wpdb->db_server_info() ); } /** * Parses a server-info string for window-function support. * * MariaDB grew window functions in 10.2, MySQL in 8.0. MariaDB may hide * behind a `5.5.5-` replication prefix, so its own version is read from * around the word MariaDB rather than from the front of the string. * * @param string $server_info Raw `db_server_info()` string. */ public static function version_supports( string $server_info ): bool { if ( false !== stripos( $server_info, 'mariadb' ) ) { if ( preg_match( '/(\d+\.\d+(?:\.\d+)?)-MariaDB/i', $server_info, $m ) ) { return version_compare( $m[1], '10.2', '>=' ); } return (bool) preg_match( '/MariaDB[^0-9]*(\d+\.\d+)/i', $server_info, $m ) && version_compare( $m[1], '10.2', '>=' ); } if ( false !== stripos( $server_info, 'sqlite' ) ) { return false; } return (bool) preg_match( '/(\d+\.\d+(?:\.\d+)?)/', $server_info, $m ) && version_compare( $m[1], '8.0', '>=' ); } }