Initial commit

This commit is contained in:
gustavooth
2026-07-23 22:56:30 -03:00
commit 22a1006598
1816 changed files with 410742 additions and 0 deletions
@@ -0,0 +1,77 @@
<?php
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/Subscribers.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/SubscriberMeta.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/SubscriberPivot.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/Campaigns.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/CampaignEmails.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/CampaignUrlMetrics.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/Lists.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/Tags.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/Meta.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/SubscriberNotes.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/UrlStores.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/Funnels.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/FunnelSequences.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/FunnelSubscribers.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/FunnelMetrics.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/Terms.php');
require_once(FLUENTCRM_PLUGIN_PATH . 'database/migrations/TermRelations.php');
class FluentCRMDBMigrator
{
public static function run($network_wide = false)
{
global $wpdb;
if ($network_wide) {
// Retrieve all site IDs from this network (WordPress >= 4.6 provides easy to use functions for that).
if (function_exists('get_sites') && function_exists('get_current_network_id')) {
$site_ids = get_sites(array('fields' => 'ids', 'network_id' => get_current_network_id()));
} else {
$site_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs WHERE site_id = $wpdb->siteid;");
}
// Install the plugin for all these sites.
foreach ($site_ids as $site_id) {
switch_to_blog($site_id);
self::migrate();
restore_current_blog();
}
} else {
self::migrate();
}
}
public static function migrate()
{
\FluentCrmMigrations\Subscribers::migrate();
\FluentCrmMigrations\SubscriberMeta::migrate();
\FluentCrmMigrations\SubscriberPivot::migrate();
\FluentCrmMigrations\Campaigns::migrate();
\FluentCrmMigrations\CampaignEmails::migrate();
\FluentCrmMigrations\CampaignUrlMetrics::migrate();
\FluentCrmMigrations\Lists::migrate();
\FluentCrmMigrations\Tags::migrate();
\FluentCrmMigrations\Meta::migrate();
\FluentCrmMigrations\SubscriberNotes::migrate();
\FluentCrmMigrations\UrlStores::migrate();
\FluentCrmMigrations\Funnels::migrate();
\FluentCrmMigrations\FunnelSequences::migrate();
\FluentCrmMigrations\FunnelSubscribers::migrate();
\FluentCrmMigrations\FunnelMetrics::migrate();
\FluentCrmMigrations\Terms::migrate();
\FluentCrmMigrations\TermRelations::migrate();
update_option('_fluentcrm_db_version', FLUENTCRM_DB_VERSION);
}
}
if (!isset($network_wide)) {
$network_wide = false;
}
FluentCRMDBMigrator::run($network_wide);
@@ -0,0 +1,2 @@
<?php
// Silence is golden.
@@ -0,0 +1,46 @@
<?php
namespace FluentCrmMigrations;
class ActivityLogsMigrator
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix . 'fc_activity_logs';
$indexPrefix = $wpdb->prefix . 'fc_al_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`object_type` VARCHAR(192) NOT NULL,
`object_id` BIGINT NULL,
`action` VARCHAR(192) NOT NULL,
`source` VARCHAR(50) DEFAULT 'wp_admin',
`description` TEXT NULL,
`activity_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_u_action_idx` (`action`),
INDEX `{$indexPrefix}_u_src_idx` (`source`)
) $charsetCollate;";
if(!function_exists('dbDelta')) {
require_once(ABSPATH.'wp-admin/includes/upgrade.php');
}
dbDelta($sql);
}
}
}
@@ -0,0 +1,107 @@
<?php
namespace FluentCrmMigrations;
class CampaignEmails
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix . 'fc_campaign_emails';
$indexPrefix = $wpdb->prefix . 'fc_cam_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`campaign_id` BIGINT UNSIGNED NULL,
`email_type` VARCHAR(50) NULL DEFAULT 'campaign',
`subscriber_id` BIGINT UNSIGNED NULL, /*Nullable because ondelete set null*/
`email_subject_id` BIGINT UNSIGNED NULL, /*FK subjects.id*/
`email_address` VARCHAR(192) NOT NULL,
`email_subject` VARCHAR(192) NULL,
`email_body` LONGTEXT NULL,
`email_headers` TEXT NULL,
`is_open` TINYINT(1) NOT NULL DEFAULT 0,
`is_parsed` TINYINT(1) NOT NULL DEFAULT 0,
`click_counter` INT NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'draft', /*sent, scheduled, pending, bounced,failed*/
`note` TEXT NULL, /* To keep the failed message */
`scheduled_at` TIMESTAMP NULL, /*for scheduled email (check status)*/
`email_hash` VARCHAR(192) NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_sid_idx` (`subscriber_id` ASC),
INDEX `{$indexPrefix}_et_idx` (`email_type` ASC),
INDEX `{$indexPrefix}_emtidx` (`email_hash` ASC),
INDEX `{$indexPrefix}_updated_at` (`updated_at`),
INDEX `{$indexPrefix}status_sc_at` (`status`, `scheduled_at`),
INDEX `{$indexPrefix}cid_status` (`campaign_id`, `status`)
) $charsetCollate;";
dbDelta($sql);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
$indexNames = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
$indexNames[] = $index->Key_name;
}
if(!in_array('scheduled_at', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE {$table} ADD INDEX `scheduled_at` (`scheduled_at`);");
}
if(!in_array('updated_at', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE {$table} ADD INDEX {$indexPrefix}_updated_at (`updated_at`);");
}
if(!in_array('email_hash', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE {$table} ADD INDEX `{$indexPrefix}_emtidx` (`email_hash`);");
}
// Composite index for batch email queries: WHERE status IN (...) AND scheduled_at <= ? ORDER BY scheduled_at
if (!in_array($indexPrefix . 'status_sc_at', $indexNames)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE {$table} ADD INDEX `{$indexPrefix}status_sc_at` (`status`, `scheduled_at`);");
}
// Composite index for campaign-scoped queries: WHERE campaign_id = ? AND status = ?
// Owned by DbPerformanceService so the migration and the runtime index
// health-check / repair path share one definition.
\FluentCrm\App\Services\DbPerformanceService::ensureCriticalIndex($indexPrefix . 'cid_status');
// Drop redundant single-column indexes now covered by composite indexes
$redundantIndexes = [
$indexPrefix . '_estidx', // status — covered by (status, scheduled_at)
$indexPrefix . '_scheduled_at', // scheduled_at (prefixed) — covered by (status, scheduled_at)
'scheduled_at', // scheduled_at (unprefixed, from earlier migration) — covered by (status, scheduled_at)
$indexPrefix . '_cid_idx', // campaign_id — covered by (campaign_id, status)
$indexPrefix . 'sc_at_status', // (scheduled_at, status) — wrong column order
];
foreach ($redundantIndexes as $indexName) {
if (in_array($indexName, $indexNames)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE {$table} DROP INDEX `{$indexName}`");
}
}
}
}
}
@@ -0,0 +1,64 @@
<?php
namespace FluentCrmMigrations;
class CampaignUrlMetrics
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_campaign_url_metrics';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`url_id` BIGINT UNSIGNED NULL,
`campaign_id` BIGINT UNSIGNED NULL,
`subscriber_id` BIGINT UNSIGNED NULL,
`type` VARCHAR(50) NULL DEFAULT 'click', /*view/click*/
`ip_address` VARCHAR(30) NULL,
`country` VARCHAR(40) NULL,
`city` VARCHAR(40) NULL,
`counter` INT UNSIGNED NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
KEY `url_id` (`url_id`),
KEY `campaign_id` (`campaign_id`),
KEY `subscriber_id` (`subscriber_id`),
KEY `type` (`type`)
) $charsetCollate;";
dbDelta($sql);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('subscriber_id', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexSql = "ALTER TABLE {$table} ADD INDEX `url_id` (`url_id`),
ADD INDEX `campaign_id` (`campaign_id`),
ADD INDEX `subscriber_id` (`subscriber_id`),
ADD INDEX `type` (`type`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($indexSql);
}
}
}
}
@@ -0,0 +1,73 @@
<?php
namespace FluentCrmMigrations;
class Campaigns
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_campaigns';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`parent_id` BIGINT UNSIGNED NULL,
`type` VARCHAR(50) NOT NULL DEFAULT 'campaign',
`title` VARCHAR(192) NOT NULL,
`available_urls` TEXT NULL,
`slug` VARCHAR(192) NOT NULL,
`status` VARCHAR(50) NOT NULL,
`template_id` BIGINT(20) UNSIGNED NULL,
`email_subject` VARCHAR(192),
`email_pre_header` VARCHAR(192),
`email_body` LONGTEXT NOT NULL,
`recipients_count` INT NOT NULL DEFAULT 0,
`delay` INT(11) NULL DEFAULT 0,
`utm_status` TINYINT(1) NULL DEFAULT 0,
`utm_source` VARCHAR(192) NULL,
`utm_medium` VARCHAR(192) NULL,
`utm_campaign` VARCHAR(192) NULL,
`utm_term` VARCHAR(192) NULL,
`utm_content` VARCHAR(192) NULL,
`design_template` VARCHAR(192) NULL,
`scheduled_at` TIMESTAMP NULL,
`settings` LONGTEXT null,
`created_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
KEY `type` (`type`),
KEY `status` (`status`),
KEY `parent_id` (`parent_id`)
) $charsetCollate;";
dbDelta($sql);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('status', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$indexSql = "ALTER TABLE {$table} ADD INDEX `type` (`type`),
ADD INDEX `status` (`status`),
ADD INDEX `parent_id` (`parent_id`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($indexSql);
}
}
}
}
@@ -0,0 +1,90 @@
<?php
namespace FluentCrmMigrations;
class CompaniesMigrator
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_companies';
$indexPrefix = $wpdb->prefix .'fc_index_';
if(!function_exists('dbDelta')) {
require_once(ABSPATH.'wp-admin/includes/upgrade.php');
}
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`hash` VARCHAR(90) NULL,
`owner_id` BIGINT UNSIGNED NULL,
`name` VARCHAR(192) NULL,
`industry` VARCHAR(192) NULL,
`email` VARCHAR(190) NULL,
`timezone` VARCHAR(192) NULL,
`address_line_1` VARCHAR(192) NULL,
`address_line_2` VARCHAR(192) NULL,
`postal_code` VARCHAR(192) NULL,
`city` VARCHAR(192) NULL,
`state` VARCHAR(192) NULL,
`country` VARCHAR(192) NULL,
`employees_number` INT UNSIGNED NULL DEFAULT 0,
`description` LONGTEXT NULL,
`phone` VARCHAR(50) NULL,
`type` VARCHAR(50) DEFAULT '', /*active, archived*/
`logo` VARCHAR(192) NULL,
`website` VARCHAR(192) NULL,
`linkedin_url` VARCHAR(192) NULL,
`facebook_url` VARCHAR(192) NULL,
`twitter_url` VARCHAR(192) NULL,
`date_of_start` DATE NULL,
`meta` LONGTEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_com_owner_id_idx` (`owner_id`),
INDEX `{$indexPrefix}_com_industry_idx` (`industry`),
INDEX `{$indexPrefix}_com_type_idx` (`type`),
INDEX `{$indexPrefix}_com_name_idx` (`name`)
) $charsetCollate;";
dbDelta($sql);
} else {
// check if meta is available in the column
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$columns = $wpdb->get_results("SHOW COLUMNS FROM $table", ARRAY_A);
$allColumns = array_column($columns, 'Field');
if (!in_array('meta', $allColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE $table ADD COLUMN `meta` LONGTEXT NULL AFTER `linkedin_url`");
}
if (!in_array('date_of_start', $allColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE $table ADD COLUMN `date_of_start` DATE NULL AFTER `linkedin_url`");
}
if (!in_array('facebook_url', $allColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE $table ADD COLUMN `facebook_url` VARCHAR(192) NULL AFTER `linkedin_url`");
}
if(!in_array('twitter_url', $allColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE $table ADD COLUMN `twitter_url` VARCHAR(192) NULL AFTER `linkedin_url`");
}
}
}
}
@@ -0,0 +1,68 @@
<?php
namespace FluentCrmMigrations;
class FunnelMetrics
{
/**
* Migrate the table.
*
* @param bool $isForced
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix . 'fc_funnel_metrics';
$indexPrefix = $wpdb->prefix . 'fc_fmx_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`funnel_id` BIGINT UNSIGNED NULL,
`sequence_id` BIGINT UNSIGNED NULL,
`subscriber_id` BIGINT UNSIGNED NULL,
`benchmark_value` BIGINT UNSIGNED DEFAULT 0,
`benchmark_currency` VARCHAR(10) DEFAULT 'USD',
`status` VARCHAR(50) DEFAULT 'completed',
`notes` TEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_m_idx` (`funnel_id` ASC),
INDEX `{$indexPrefix}_ms__idx` (`subscriber_id` ASC),
KEY `sequence_id` (`sequence_id`),
KEY `status` (`status`),
UNIQUE KEY `funnel_seq_subscriber_unique` (`funnel_id`, `sequence_id`, `subscriber_id`)
) $charsetCollate;";
dbDelta($sql);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('sequence_id', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexSql = "ALTER TABLE {$table} ADD INDEX `sequence_id` (`sequence_id`),
ADD INDEX `status` (`status`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($indexSql);
}
// Composite unique index for idempotency enforcement. The sweep +
// dedupe + ADD UNIQUE KEY convergence lives in one place —
// DbPerformanceService, shared with the runtime index health-check /
// repair path — so the two can never drift.
\FluentCrm\App\Services\DbPerformanceService::ensureCriticalIndex('funnel_seq_subscriber_unique');
}
}
}
@@ -0,0 +1,84 @@
<?php
namespace FluentCrmMigrations;
class FunnelSequences
{
/**
* Migrate the table.
*
* @param bool $isForced
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix . 'fc_funnel_sequences';
$indexPrefix = $wpdb->prefix . 'fc_fq_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`funnel_id` BIGINT UNSIGNED NULL,
`parent_id` BIGINT UNSIGNED DEFAULT 0,
`action_name` VARCHAR(192) NULL,
`condition_type` VARCHAR(192) NULL,
`type` VARCHAR(50) DEFAULT 'sequence',
`title` VARCHAR(192) NULL,
`description` VARCHAR(192) NULL,
`status` VARCHAR(50) NULL DEFAULT 'draft',
`conditions` TEXT,
`settings` TEXT,
`note` TEXT,
`delay` INT UNSIGNED NULL,
`c_delay` INT UNSIGNED NULL,
`sequence` INT UNSIGNED NULL,
`created_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_fs_idx` (`status` ASC),
INDEX `{$indexPrefix}_fid_idx` (`funnel_id` ASC),
KEY `c_delay` (`c_delay`),
KEY `sequence` (`sequence`),
KEY `action_name` (`action_name`),
KEY `type_action_name_idx` (`type`, `action_name`)
) $charsetCollate;";
dbDelta($sql);
} else {
$sequenceTable = $wpdb->prefix.'fc_funnel_sequences';
$isMigrated = $wpdb->get_col($wpdb->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND COLUMN_NAME='parent_id' AND TABLE_NAME=%s", $sequenceTable));
if(!$isMigrated) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE {$sequenceTable} ADD COLUMN `parent_id` bigint NOT NULL DEFAULT '0', ADD `condition_type` varchar(192) NULL AFTER `parent_id`");
}
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('action_name', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexSql = "ALTER TABLE {$table} ADD INDEX `c_delay` (`c_delay`),
ADD INDEX `sequence` (`sequence`),
ADD INDEX `action_name` (`action_name`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($indexSql);
}
// Composite index for benchmark lookups (type + action_name). Created
// via DbPerformanceService so the migration and the runtime index
// health-check / repair path share one definition.
\FluentCrm\App\Services\DbPerformanceService::ensureCriticalIndex('type_action_name_idx');
}
}
}
@@ -0,0 +1,88 @@
<?php
namespace FluentCrmMigrations;
class FunnelSubscribers
{
/**
* Migrate the table.
*
* @param bool $isForced
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_funnel_subscribers';
$indexPrefix = $wpdb->prefix .'fc_fsx_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`funnel_id` BIGINT UNSIGNED NULL,
`starting_sequence_id` BIGINT UNSIGNED NULL,
`next_sequence` BIGINT UNSIGNED NULL,
`subscriber_id` BIGINT UNSIGNED NULL,
`last_sequence_id` BIGINT UNSIGNED NULL,
`next_sequence_id` BIGINT UNSIGNED NULL,
`last_sequence_status` VARCHAR(50) DEFAULT 'pending',
`status` VARCHAR(50) DEFAULT 'active',
`type` VARCHAR(50) DEFAULT 'funnel',
`last_executed_time` TIMESTAMP NULL,
`next_execution_time` TIMESTAMP NULL,
`notes` TEXT NULL,
`source_trigger_name` VARCHAR(192) NULL,
`source_ref_id` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_fidx` (`funnel_id` ASC),
INDEX `{$indexPrefix}_fsq_idx` (`subscriber_id` ASC),
KEY `status` (`status`),
KEY `type` (`type`),
KEY `next_execution_time` (`next_execution_time`),
KEY `next_sequence` (`next_sequence`),
UNIQUE KEY `funnel_subscriber_idx` (`funnel_id`, `subscriber_id`),
KEY `status_next_exec_idx` (`status`, `next_execution_time`)
) $charsetCollate;";
dbDelta($sql);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('status', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$sql = "ALTER TABLE {$table} ADD INDEX `status` (`status`),
ADD INDEX `type` (`type`),
ADD INDEX `next_execution_time` (`next_execution_time`),
ADD INDEX `next_sequence` (`next_sequence`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($sql);
}
// Two critical indexes on this table are owned by DbPerformanceService
// so the migration and the runtime index health-check / repair path
// share one definition and can never drift:
// - funnel_subscriber_idx: UNIQUE (funnel_id, subscriber_id). Its
// sweep + progress-aware dedupe + drop/re-add convergence (an old
// non-unique index of the same name OR a missing key both resolve
// to "unique key present") lives in the service.
// - status_next_exec_idx: composite index for the cron heartbeat
// query (runs every 60 seconds).
\FluentCrm\App\Services\DbPerformanceService::ensureCriticalIndex('funnel_subscriber_idx');
\FluentCrm\App\Services\DbPerformanceService::ensureCriticalIndex('status_next_exec_idx');
}
}
}
@@ -0,0 +1,58 @@
<?php
namespace FluentCrmMigrations;
class Funnels
{
/**
* Migrate the table.
*
* @param bool $isForced
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_funnels';
$indexPrefix = $wpdb->prefix .'fc_fn_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`type` VARCHAR(50) NOT NULL DEFAULT 'funnel',
`title` VARCHAR(192) NOT NULL,
`trigger_name` VARCHAR(150) NULL,
`status` VARCHAR(50) NULL DEFAULT 'draft',
`conditions` TEXT,
`settings` TEXT,
`created_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_f_idx` (`status` ASC),
INDEX `{$indexPrefix}_ft_idx` (`trigger_name` ASC),
KEY `type` (`type`)
) $charsetCollate;";
dbDelta($sql);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('type', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "ALTER TABLE {$table} ADD INDEX `type` (`type`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($sql);
}
}
}
}
@@ -0,0 +1,37 @@
<?php
namespace FluentCrmMigrations;
class Lists
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_lists';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`title` VARCHAR(192) NOT NULL,
`slug` VARCHAR(192) NOT NULL,
`description` TINYTEXT NULL,
`is_public` TINYINT(1) DEFAULT 0,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL
) $charsetCollate;";
dbDelta($sql);
}
}
}
@@ -0,0 +1,51 @@
<?php
namespace FluentCrmMigrations;
class Meta
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_meta';
$indexPrefix = $wpdb->prefix .'fc_mt_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`object_type` VARCHAR(50) NOT NULL,
`object_id` BIGINT NULL,
`key` VARCHAR(192) NOT NULL,
`value` LONGTEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_mt_idx` (`object_type` ASC),
INDEX `{$indexPrefix}_mto_id_idx` (`object_id` ASC),
INDEX `{$indexPrefix}_mto_id_key` (`key` )
) $charsetCollate;";
dbDelta($sql);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('key', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("ALTER TABLE {$table} ADD INDEX `{$indexPrefix}_mto_id_key` (`key`);");
}
}
}
}
@@ -0,0 +1,49 @@
<?php
namespace FluentCrmMigrations;
class SubscriberEventTracking
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix . 'fc_event_tracking';
$indexPrefix = $wpdb->prefix . 'fc_et_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`subscriber_id` BIGINT UNSIGNED NOT NULL,
`counter` INT(11) UNSIGNED NULL DEFAULT 1,
`created_by` BIGINT UNSIGNED NULL,
`provider` VARCHAR(50) DEFAULT 'custom',
`event_key` VARCHAR(192) NOT NULL,
`title` VARCHAR(192) NOT NULL,
`value` TEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_s_id_idx` (`subscriber_id`),
INDEX `{$indexPrefix}_s_idx` (`event_key`),
INDEX `{$indexPrefix}_s_idx_title` (`title`)
) $charsetCollate;";
if(!function_exists('dbDelta')) {
require_once(ABSPATH.'wp-admin/includes/upgrade.php');
}
dbDelta($sql);
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace FluentCrmMigrations;
class SubscriberMeta
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_subscriber_meta';
$indexPrefix = $wpdb->prefix .'fc_index_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`subscriber_id` BIGINT UNSIGNED NOT NULL,
`created_by` BIGINT UNSIGNED NOT NULL,
`object_type` VARCHAR(50) DEFAULT 'option',
`key` VARCHAR(192) NOT NULL,
`value` LONGTEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_s_meta_id_idx` (`subscriber_id` ASC),
INDEX `{$indexPrefix}_s_ot_idx` (`object_type` ASC)
) $charsetCollate;";
dbDelta($sql);
} else{
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('object_type', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "ALTER TABLE {$table} ADD INDEX `object_type` (`object_type`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($sql);
}
}
}
}
@@ -0,0 +1,65 @@
<?php
namespace FluentCrmMigrations;
class SubscriberNotes
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_subscriber_notes';
$indexPrefix = $wpdb->prefix .'fc_sn_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`subscriber_id` BIGINT UNSIGNED NOT NULL,
`parent_id` BIGINT UNSIGNED NULL,
`created_by` BIGINT UNSIGNED NULL,
`status` VARCHAR(50) DEFAULT 'open',
`type` VARCHAR(50) DEFAULT 'note',
`is_private` TINYINT DEFAULT 1,
`title` VARCHAR(192) NULL,
`description` LONGTEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_s_id_idx` (`subscriber_id` ASC),
INDEX `{$indexPrefix}_s_idx` (`status` ASC),
KEY `type` (`type`)
) $charsetCollate;";
dbDelta($sql);
} else {
$charsetCollate = $wpdb->collate;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "ALTER TABLE {$table} CHANGE `description` `description` longtext COLLATE '".$charsetCollate."' NULL AFTER `title`;";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($sql);
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('type', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexSql = "ALTER TABLE `{$table}` ADD INDEX `type` (`type`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($indexSql);
}
}
}
}
@@ -0,0 +1,62 @@
<?php
namespace FluentCrmMigrations;
class SubscriberPivot
{
/**
* Migrate the table.
*
* This table will maintain many-to-many relationships
* between subscriber & lists and subscriber & tags.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_subscriber_pivot';
$subscriberTable = $wpdb->prefix .'fc_subscribers';
$indexPrefix = $wpdb->prefix .'fc_srp_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`subscriber_id` BIGINT UNSIGNED NOT NULL,
`object_id` BIGINT UNSIGNED NOT NULL, /*list_id or tag_id*/
`object_type` VARCHAR(50) NOT NULL, /*list or tag*/
`status` VARCHAR(50) NULL,
`is_public` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_sp_id_idx` (`subscriber_id` ASC),
INDEX `{$indexPrefix}_sp_o_id_idx` (`object_id` ASC),
INDEX `{$indexPrefix}_sp_t_id_idx` (`object_type` ASC),
UNIQUE KEY `subscriber_object_type_unique` (`subscriber_id`, `object_id`, `object_type`(50))
) $charsetCollate;";
dbDelta($sql);
return;
}
// Existing table — ensure the composite unique key is present so that
// attachTags/attachLists can rely on INSERT IGNORE for race-safe upsert.
// The unique key prevents two concurrent attach paths (e.g. WooCommerce
// + WP Fusion + LearnDash hooks firing on the same enrollment) from
// both inserting a duplicate (subscriber_id, object_id, object_type)
// row and both firing the corresponding contact_added_to_* action.
//
// The sweep + dedupe + online ADD UNIQUE KEY convergence lives in one
// place — DbPerformanceService, which is also what the runtime index
// health-check / repair path uses — so the two can never drift.
\FluentCrm\App\Services\DbPerformanceService::ensureCriticalIndex('subscriber_object_type_unique');
}
}
@@ -0,0 +1,71 @@
<?php
namespace FluentCrmMigrations;
class Subscribers
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_subscribers';
$indexPrefix = $wpdb->prefix .'fc_index_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NULL,
`hash` VARCHAR(90) NULL,
`contact_owner` BIGINT UNSIGNED NULL,
`company_id` BIGINT UNSIGNED NULL,
`prefix` VARCHAR(192) NULL,
`first_name` VARCHAR(192) NULL,
`last_name` VARCHAR(192) NULL,
`email` VARCHAR(190) NOT NULL UNIQUE,
`timezone` VARCHAR(192) NULL,
`address_line_1` VARCHAR(192) NULL,
`address_line_2` VARCHAR(192) NULL,
`postal_code` VARCHAR(192) NULL,
`city` VARCHAR(192) NULL,
`state` VARCHAR(192) NULL,
`country` VARCHAR(192) NULL,
`ip` VARCHAR(40) NULL,
`latitude` DECIMAL(10, 8) NULL,
`longitude` DECIMAL(10, 8) NULL,
`total_points` INT UNSIGNED NOT NULL DEFAULT 0,
`life_time_value` INT UNSIGNED NOT NULL DEFAULT 0,
`phone` VARCHAR(50) NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'subscribed', /*subscribed, unsubscribed, pending, bounced*/
`contact_type` VARCHAR(50) DEFAULT 'lead', /*lead, customer*/
`source` VARCHAR(50) NULL,
`avatar` VARCHAR(192) NULL,
`date_of_birth` DATE NULL,
`created_at` TIMESTAMP NULL,
`last_activity` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_subscriber_user_id_idx` (`user_id` ASC),
INDEX `{$indexPrefix}_subscriber_status_idx` (`status` ASC)
) $charsetCollate;";
dbDelta($sql);
} else {
// change ip column from 20 to 40 to support ipv6
$column_name = 'ip';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "ALTER TABLE {$table} MODIFY {$column_name} VARCHAR(40) NULL;";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($sql);
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace FluentCrmMigrations;
class Tags
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_tags';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`title` VARCHAR(192) NOT NULL,
`slug` VARCHAR(192) NOT NULL,
`description` TINYTEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL
) $charsetCollate;";
dbDelta($sql);
}
}
}
@@ -0,0 +1,39 @@
<?php
namespace FluentCrmMigrations;
class TermRelations
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_term_relations';
$indexPrefix = $wpdb->prefix .'fc_tmr_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`term_id` BIGINT UNSIGNED NULL,
`object_type` VARCHAR(192) NOT NULL,
`object_id` BIGINT NULL,
`settings` LONGTEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_tm_idx` (`term_id` ASC),
INDEX `{$indexPrefix}_tm_id_type` (`object_type` ASC),
INDEX `{$indexPrefix}_tm_id_idx` (`object_id`)
) $charsetCollate;";
dbDelta($sql);
}
}
}
@@ -0,0 +1,42 @@
<?php
namespace FluentCrmMigrations;
class Terms
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_terms';
$indexPrefix = $wpdb->prefix .'fc_tms_';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`parent_id` BIGINT UNSIGNED NULL,
`taxonomy_name` VARCHAR(50) NOT NULL,
`slug` VARCHAR(100) NOT NULL,
`title` TEXT NULL,
`position` DECIMAL(10,2) DEFAULT 1.00 NOT NULL,
`description` LONGTEXT NULL,
`settings` LONGTEXT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `{$indexPrefix}_tm_idx` (`taxonomy_name` ASC),
INDEX `{$indexPrefix}_tm_id_slug` (`slug` ASC),
INDEX `{$indexPrefix}_tm_id_pid` (`parent_id` )
) $charsetCollate;";
dbDelta($sql);
}
}
}
@@ -0,0 +1,102 @@
<?php
namespace FluentCrmMigrations;
class UrlStores
{
/**
* Migrate the table.
*
* @return void
*/
public static function migrate()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$table = $wpdb->prefix .'fc_url_stores';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = "CREATE TABLE $table (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`url` TEXT NOT NULL,
`short` VARCHAR(50) NOT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
UNIQUE KEY `short` (`short`),
KEY `url` (`url`(191))
) $charsetCollate;";
dbDelta($sql);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$indexes = $wpdb->get_results("SHOW INDEX FROM $table");
$indexedColumns = [];
foreach ($indexes as $index) {
$indexedColumns[] = $index->Column_name;
}
if(!in_array('short', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$sql = "ALTER TABLE {$table} ADD UNIQUE INDEX `short` (`short`);";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($sql);
} else {
// Upgrade existing non-unique index to unique
$isUnique = false;
foreach ($indexes as $index) {
if ($index->Column_name === 'short' && $index->Non_unique == 0) {
$isUnique = true;
break;
}
}
if (!$isUnique) {
// Check if duplicates actually exist before running heavy queries
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$hasDuplicates = $wpdb->get_var("SELECT COUNT(*) FROM (SELECT `short` FROM {$table} GROUP BY `short` HAVING COUNT(*) > 1) AS dupes");
if ($hasDuplicates) {
$metricsTable = $wpdb->prefix . 'fc_campaign_url_metrics';
// Re-point metrics from duplicate rows to the kept (highest id) row before cleanup
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$repointed = $wpdb->query("UPDATE {$metricsTable} m INNER JOIN {$table} t1 ON m.url_id = t1.id INNER JOIN (SELECT `short`, MAX(id) AS max_id FROM {$table} GROUP BY `short` HAVING COUNT(*) > 1) dups ON t1.`short` = dups.`short` AND t1.id < dups.max_id SET m.url_id = dups.max_id");
// Only delete duplicates if re-point succeeded (false = query error)
if ($repointed === false) {
return;
}
// Clean up duplicate short values (keep highest id)
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$cleaned = $wpdb->query("DELETE t1 FROM {$table} t1 INNER JOIN {$table} t2 WHERE t1.`short` = t2.`short` AND t1.id < t2.id");
if ($cleaned === false) {
return;
}
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query("ALTER TABLE {$table} DROP INDEX `short`, ADD UNIQUE INDEX `short` (`short`);");
}
}
if(!in_array('url', $indexedColumns)) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query("ALTER TABLE {$table} ADD INDEX `url` (`url`(191));");
}
// change column type from tinytext to text - for already installed sites
$column_name = 'url';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$dataType = $wpdb->get_row("describe {$table} {$column_name}");
if($dataType->Type == 'tinytext') {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$sql = "ALTER TABLE {$table} MODIFY {$column_name} TEXT NOT NULL;";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query($sql);
}
}
}
}
@@ -0,0 +1,3 @@
<?php
// Silence is golden