query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table ) ); } // Defensive: a rebuild interrupted mid-swap (see StemFrequencyRepository // ::replace_all()) can leave a throwaway staging or previous-live table // behind. Neither is in TableNames::all() -- they are deliberately not // part of the migrated schema -- so they need their own explicit cleanup // here. $stem_frequency_table = TableNames::stem_frequency(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Intentional uninstall cleanup. $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $stem_frequency_table . '_staging' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Intentional uninstall cleanup. $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $stem_frequency_table . '_previous' ) ); // Delete every postlinkly_-prefixed option except those the Pro // add-on owns (provider keys, AI settings, embedding index state). // Deleting and reinstalling this plugin is a common troubleshooting // step and must not wipe a customer's paid-feature setup; Pro's own // uninstaller removes these. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'postlinkly\\_%' ESCAPE '\\\\' AND option_name NOT LIKE 'postlinkly\\_ai\\_%' ESCAPE '\\\\' AND option_name NOT LIKE 'postlinkly\\_embedding\\_%' ESCAPE '\\\\' AND option_name NOT LIKE 'postlinkly\\_pro\\_%' ESCAPE '\\\\' AND option_name <> 'postlinkly_text_provider_state'" ); // Delete every postlinkly_-prefixed transient (in options), plus the // suggestion cache's own `pl_sugg_` transients — that cache predates // the postlinkly_ prefix convention and uses a shorter key to stay // under the transient name length limit, so it needs its own patterns. $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_postlinkly\\_%' ESCAPE '\\\\' OR option_name LIKE '\\_transient\\_timeout\\_postlinkly\\_%' ESCAPE '\\\\' OR option_name LIKE '\\_transient\\_pl\\_sugg\\_%' ESCAPE '\\\\' OR option_name LIKE '\\_transient\\_timeout\\_pl\\_sugg\\_%' ESCAPE '\\\\'" ); // Delete every postlinkly_-prefixed post meta, plus the hidden // (leading-underscore) variant WordPress uses to keep a meta key out // of the classic-editor custom-fields UI, e.g. `_postlinkly_indexed_stems` // (see StemFrequencyIndexer). A bare `postlinkly\_%` pattern does not // match a key that starts with an underscore before the prefix. $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE 'postlinkly\\_%' ESCAPE '\\\\' OR meta_key LIKE '\\_postlinkly\\_%' ESCAPE '\\\\'" ); // Delete every postlinkly_-prefixed user meta. $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'postlinkly\\_%' ESCAPE '\\\\'" ); // phpcs:enable // Term meta is always present on WP 6.4+ (our minimum), but guard // defensively in case a host serves a stripped-down environment. $termmeta = $wpdb->termmeta; if ( '' !== $termmeta ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "DELETE FROM {$termmeta} WHERE meta_key LIKE 'postlinkly\\_%' ESCAPE '\\\\'" ); // phpcs:enable } } }