PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/database/class-database-schema.php +27 -155 2.3.02.7.0 View file →
@@ -244,35 +244,14 @@
244 244 ],
245 245 'foreign_keys' => []
246 246 ],
247 247
248 - // === AI VISIBILITY TABLES (2) ===
248 + // === AI VISIBILITY TABLES (1) ===
249 249 'ai_traffic' => [
250 250 'description' => 'Daily aggregate counters for AI referral traffic, AI crawler hits, and the all-traffic baseline',
251 251 'primary_key' => 'id',
252 252 'indexes' => ['day', 'kind'],
253 253 'foreign_keys' => []
254 - ],
255 - 'brand_visibility_checks' => [
256 - 'description' => 'History of AI brand-visibility checks run through the configured AI provider',
257 - 'primary_key' => 'id',
258 - 'indexes' => ['checked_at', 'query_text'],
259 - 'foreign_keys' => []
260 - ],
261 - 'bv_runs' => [
262 - 'description' => 'Brand Visibility v2 analysis runs: one row per run, with its config snapshot, progress counters and computed aggregates',
263 - 'primary_key' => 'id',
264 - 'indexes' => ['status', 'started_at', 'finished_at'],
265 - 'foreign_keys' => []
266 - ],
267 - 'bv_tasks' => [
268 - 'description' => 'Brand Visibility v2 units of work: one row per query x platform x sample, processed off-request by cron ticks',
269 - 'primary_key' => 'id',
270 - 'indexes' => ['run_id', 'status'],
271 - 'composite_indexes' => [
272 - 'run_status' => ['run_id', 'status'],
273 - ],
274 - 'foreign_keys' => []
275 254 ]
276 255 ];
277 256
278 257 /**
@@ -294,9 +273,9 @@
294 273 'ai' => ['ai_cache', 'ai_usage'],
295 274 'content' => ['content_briefs'],
296 275 'scoring' => ['seo_scores'],
297 276 'reporting' => ['email_report_logs'],
298 - 'ai_visibility' => ['ai_traffic', 'brand_visibility_checks', 'bv_runs', 'bv_tasks']
277 + 'ai_visibility' => ['ai_traffic']
299 278 ];
300 279
301 280 /**
302 281 * Constructor
@@ -346,8 +325,14 @@
346 325
347 326 // Create table using dbDelta for WordPress compatibility
348 327 $result = dbDelta($sql);
349 328
329 + // dbDelta reports nothing when the database refuses the
330 + // statement, so wpdb's own error is the only account of why.
331 + // Read it now: table_exists() runs a query of its own, and
332 + // every wpdb query starts by clearing last_error.
333 + $db_error = (string) $this->wpdb->last_error;
334 +
350 335 // Verify table creation
351 336 if ($this->table_exists($full_table_name)) {
352 337 $results['tables_created'][] = $full_table_name;
353 338
@@ -356,12 +341,8 @@
356 341
357 342 // Add constraints if needed
358 343 $this->add_table_constraints($table_name);
359 344 } else {
360 - // dbDelta reports nothing when the database refuses the
361 - // statement, so wpdb's own error is the only account of why.
362 - $db_error = (string) $this->wpdb->last_error;
363 -
364 345 $results['tables_failed'][] = $full_table_name;
365 346 $results['errors'][] = "Failed to create table: {$full_table_name}"
366 347 . ('' !== $db_error ? ' — ' . $db_error : '');
367 348 $results['success'] = false;
@@ -684,10 +665,16 @@
684 665 * @throws \InvalidArgumentException On failure.
685 666 */
686 667 private function get_table_sql(string $table_name): string {
687 668 $full_table_name = $this->get_table_name($table_name);
688 - $charset_collate = $this->db_config['charset_collate'];
689 669
670 + // Pin the engine instead of inheriting the server's
671 + // default_storage_engine. The indexes below assume InnoDB: MyISAM caps
672 + // a key at 1000 bytes (seo_settings and seo_social exceed it) and
673 + // rejects descending indexes (seo_schema), so on a server defaulting
674 + // to MyISAM those tables were never created (#725).
675 + $charset_collate = trim('ENGINE=InnoDB ' . $this->db_config['charset_collate']);
676 +
690 677 switch ($table_name) {
691 678 // SEO Tables
692 679 case 'seo_settings':
693 680 return $this->get_seo_settings_table_sql($full_table_name, $charset_collate);
@@ -720,14 +707,8 @@
720 707
721 708 // AI Visibility Tables
722 709 case 'ai_traffic':
723 710 return $this->get_ai_traffic_table_sql($full_table_name, $charset_collate);
724 - case 'bv_runs':
725 - return $this->get_bv_runs_table_sql($full_table_name, $charset_collate);
726 - case 'bv_tasks':
727 - return $this->get_bv_tasks_table_sql($full_table_name, $charset_collate);
728 - case 'brand_visibility_checks':
729 - return $this->get_brand_visibility_checks_table_sql($full_table_name, $charset_collate);
730 711
731 712 default:
732 713 throw new \InvalidArgumentException('Unknown table: ' . esc_html($table_name));
733 714 }
@@ -1752,34 +1733,29 @@
1752 1733 return (int) $result > 0;
1753 1734 }
1754 1735
1755 1736 /**
1756 - * Check if MySQL supports JSON column type with caching
1737 + * Check if the database server supports the JSON column type
1757 1738 *
1758 - * Uses WordPress's built-in database version detection and caches the result
1759 - * to avoid repeated database queries during schema creation.
1739 + * MySQL added JSON in 5.7.8 and MariaDB in 10.2.7. MariaDB's own 10.x
1740 + * number passes any MySQL threshold, and on older PHP the server string
1741 + * carries a `5.5.5-` replication prefix that db_version() reads as the
1742 + * version, so MariaDB's version is taken from the server string itself.
1743 + * Neither lookup queries the database.
1760 1744 *
1761 1745 * @since 1.0.0
1762 - * @return bool True if MySQL 5.7+ supports JSON columns
1746 + * @return bool True if the server supports JSON columns
1763 1747 */
1764 1748 private function get_mysql_json_support(): bool {
1765 - // Check if we have cached result
1766 - static $json_support = null;
1767 -
1768 - if ($json_support !== null) {
1769 - return $json_support;
1770 - }
1771 -
1772 - // Use WordPress's built-in database version method
1773 1749 global $wpdb;
1774 1750
1775 - // Get MySQL version using WordPress method (safer than direct query)
1776 - $mysql_version = $wpdb->db_version();
1751 + $server_info = method_exists($wpdb, 'db_server_info') ? (string) $wpdb->db_server_info() : '';
1777 1752
1778 - // Cache the result for subsequent calls
1779 - $json_support = version_compare($mysql_version, '5.7.0', '>=');
1753 + if (preg_match('/(\d+(?:\.\d+)+)-MariaDB/i', $server_info, $matches)) {
1754 + return version_compare($matches[1], '10.2.7', '>=');
1755 + }
1780 1756
1781 - return $json_support;
1757 + return version_compare((string) $wpdb->db_version(), '5.7.8', '>=');
1782 1758 }
1783 1759
1784 1760 /**
1785 1761 * Get SQL for the AI traffic table.
@@ -1807,111 +1783,7 @@
1807 1783 PRIMARY KEY (id),
1808 1784 UNIQUE KEY uniq_bucket (day, kind, source, path),
1809 1785 KEY idx_day (day),
1810 1786 KEY idx_kind (kind)
1811 - ) {$charset_collate};";
1812 - }
1813 -
1814 - /**
1815 - * Get SQL for the brand visibility checks table.
1816 - *
1817 - * One row per (query, check run): whether the AI provider's answer
1818 - * mentioned the brand and/or cited the site's domain, plus a short
1819 - * excerpt for context.
1820 - *
1821 - * @since 1.27.0
1822 - *
1823 - * @param string $table_name Full table name
1824 - * @param string $charset_collate Charset and collation
1825 - * @return string SQL for table creation
1826 - */
1827 - private function get_brand_visibility_checks_table_sql(string $table_name, string $charset_collate): string {
1828 - return "CREATE TABLE `{$table_name}` (
1829 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1830 - checked_at datetime NOT NULL,
1831 - query_text varchar(191) NOT NULL,
1832 - provider varchar(20) NOT NULL DEFAULT '',
1833 - model varchar(80) NOT NULL DEFAULT '',
1834 - mentioned tinyint(1) NOT NULL DEFAULT 0,
1835 - cited tinyint(1) NOT NULL DEFAULT 0,
1836 - excerpt text NULL,
1837 - answer longtext NULL,
1838 - PRIMARY KEY (id),
1839 - KEY idx_checked (checked_at),
1840 - KEY idx_query (query_text)
1841 - ) {$charset_collate};";
1842 - }
1843 -
1844 - /**
1845 - * Brand Visibility v2 — analysis runs.
1846 - *
1847 - * One row per "Run analysis". `config` snapshots the brand profile,
1848 - * competitors, queries and platforms the run was started with, so a run's
1849 - * results stay interpretable after the user edits their setup. `results`
1850 - * holds the computed aggregates (index, mention rate, share of voice,
1851 - * per-platform and per-query breakdowns) written once by the finalizer.
1852 - *
1853 - * @since 1.28.0
1854 - *
1855 - * @param string $table_name Full table name.
1856 - * @param string $charset_collate Charset/collation clause.
1857 - * @return string CREATE TABLE statement.
1858 - */
1859 - private function get_bv_runs_table_sql(string $table_name, string $charset_collate): string {
1860 - return "CREATE TABLE `{$table_name}` (
1861 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1862 - status varchar(20) NOT NULL DEFAULT 'queued',
1863 - started_at datetime NOT NULL,
1864 - finished_at datetime NULL,
1865 - tasks_total int(11) NOT NULL DEFAULT 0,
1866 - tasks_done int(11) NOT NULL DEFAULT 0,
1867 - tasks_failed int(11) NOT NULL DEFAULT 0,
1868 - config longtext NULL,
1869 - results longtext NULL,
1870 - error text NULL,
1871 - PRIMARY KEY (id),
1872 - KEY idx_status (status),
1873 - KEY idx_started (started_at),
1874 - KEY idx_finished (finished_at)
1875 - ) {$charset_collate};";
1876 - }
1877 -
1878 - /**
1879 - * Brand Visibility v2 — individual probe tasks.
1880 - *
1881 - * One row per (query x platform x sample). Sampling is the whole point:
1882 - * a single LLM answer is noise, so a mention rate is only meaningful as
1883 - * mentions/samples. Rows are processed off-request by cron ticks, which is
1884 - * what keeps a 100+ call run from timing out a REST request, and what lets
1885 - * an interrupted run resume instead of restarting.
1886 - *
1887 - * @since 1.28.0
1888 - *
1889 - * @param string $table_name Full table name.
1890 - * @param string $charset_collate Charset/collation clause.
1891 - * @return string CREATE TABLE statement.
1892 - */
1893 - private function get_bv_tasks_table_sql(string $table_name, string $charset_collate): string {
1894 - return "CREATE TABLE `{$table_name}` (
1895 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1896 - run_id bigint(20) unsigned NOT NULL,
1897 - query_text varchar(500) NOT NULL,
1898 - query_type varchar(20) NOT NULL DEFAULT 'branded',
1899 - platform varchar(20) NOT NULL DEFAULT '',
1900 - sample_index tinyint(3) unsigned NOT NULL DEFAULT 0,
1901 - status varchar(20) NOT NULL DEFAULT 'pending',
1902 - attempts tinyint(3) unsigned NOT NULL DEFAULT 0,
1903 - mentioned tinyint(1) NOT NULL DEFAULT 0,
1904 - cited tinyint(1) NOT NULL DEFAULT 0,
1905 - sentiment varchar(10) NOT NULL DEFAULT '',
1906 - competitors text NULL,
1907 - excerpt text NULL,
1908 - answer longtext NULL,
1909 - error text NULL,
1910 - updated_at datetime NULL,
1911 - PRIMARY KEY (id),
1912 - KEY idx_run (run_id),
1913 - KEY idx_status (status),
1914 - KEY run_status (run_id, status)
1915 1787 ) {$charset_collate};";
1916 1788 }
1917 1789 }