array( 'batch_uuid' => 'char(36)', 'actor_id' => 'bigint unsigned', 'command_hash' => 'char(64)', 'file_digest' => 'char(64)', 'mapping_json' => 'longtext', 'revision_hash' => 'char(64)', 'kind' => 'varchar(20)', 'state' => 'varchar(20)', 'created_at' => 'datetime', 'updated_at' => 'datetime' ), 'import_rows' => array( 'batch_uuid' => 'char(36)', 'ordinal' => 'int unsigned', 'source_hash' => 'char(64)', 'normalized_hash' => 'char(64)', 'state' => 'varchar(20)', 'ownership_json' => 'longtext' ) ); if ( ! OptionalStoreSchema::present( $read, 'abientot_import_schema_version', '1', $definitions, array( 'import_batches' => array( array( 'batch_uuid' ) ), 'import_rows' => array( array( 'batch_uuid', 'ordinal' ), array( 'source_hash' ) ) ) ) ) { return false; } global $wpdb; foreach ( $definitions as$table => $columns ) { $engine = Db::rows( $wpdb->suppress_errors( true ), $wpdb->get_results( $wpdb->prepare( 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s', $read->table( $table ) ), ARRAY_A ), 'retained_capacity_storage_unavailable' ); if ( strtoupper( (string) ( $engine[0]['ENGINE'] ?? '' ) ) !== 'INNODB' ) { throw new \RuntimeException( 'import_retained_engine' ); } foreach ( Db::rows( $wpdb->suppress_errors( true ), $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %i', $read->table( $table ) ), ARRAY_A ), 'retained_capacity_storage_unavailable' )as$column ) { if ( isset( $columns[ $column['Field'] ] ) && $column['Null'] !== 'NO' ) { throw new \RuntimeException( 'import_retained_nullability' ); } } } return true; } private function write( $result ): void { global $wpdb; if ( $result === false || $wpdb->last_error !== '' ) { throw new \RuntimeException( 'import_retained_write' ); } } public function erase_customer( int $customer ): void { if ( ! $this->present() ) { return; } if ( ! BookingLock::is_held() ) { throw new \RuntimeException( 'import_retained_owner' ); } global $wpdb; $read = new ResourceCapacityRepository(); $rows = Db::rows( $wpdb->suppress_errors( true ), $wpdb->get_results( $wpdb->prepare( "SELECT r.batch_uuid,r.ordinal FROM %i r WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(r.ownership_json,'$.result.customer_id')) AS UNSIGNED)=%d OR EXISTS (SELECT 1 FROM %i a WHERE a.id=CAST(JSON_UNQUOTE(JSON_EXTRACT(r.ownership_json,'$.result.appointment_id')) AS UNSIGNED) AND a.customer_id=%d)", $read->table( 'import_rows' ), $customer, $read->table( 'appointments' ), $customer ), ARRAY_A ), 'retained_capacity_storage_unavailable' ); foreach ( $rows as$row ) { $tombstone = hash( 'sha256', 'erased-import:' . $row['batch_uuid'] . ':' . $row['ordinal'] ); $this->write( $wpdb->update( $read->table( 'import_rows' ), array( 'state' => 'erased', 'source_hash' => $tombstone, 'normalized_hash' => '', 'ownership_json' => '{"created":[],"result":[]}' ), array( 'batch_uuid' => $row['batch_uuid'], 'ordinal' => (int) $row['ordinal'] ) ) ); $this->write( $wpdb->update( $read->table( 'import_batches' ), array( 'state' => 'erased', 'command_hash' => '', 'file_digest' => '', 'mapping_json' => '[]', 'revision_hash' => '', 'updated_at' => gmdate( 'Y-m-d H:i:s' ) ), array( 'batch_uuid' => $row['batch_uuid'] ) ) ); } $this->revoke_previews(); } public function revoke_previews(): void { if ( ! $this->present() ) { return; } if ( ! BookingLock::is_held() ) { throw new \RuntimeException( 'import_retained_owner' ); } global $wpdb; $engine = Db::rows( $wpdb->suppress_errors( true ), $wpdb->get_results( $wpdb->prepare( 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s', $wpdb->options ), ARRAY_A ), 'retained_capacity_storage_unavailable' ); if ( strtoupper( (string) ( $engine[0]['ENGINE'] ?? '' ) ) !== 'INNODB' ) { throw new \RuntimeException( 'import_retained_option_engine' ); } // Avoid option-cache state outliving a rolled-back database epoch. The owning eraser clears it after COMMIT/ROLLBACK. $epoch = bin2hex( random_bytes( 32 ) ); $this->write( $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->options} (option_name,option_value,autoload) VALUES (%s,%s,%s) ON DUPLICATE KEY UPDATE option_value=VALUES(option_value),autoload=VALUES(autoload)", 'abientot_import_preview_epoch', $epoch, 'off' ) ) ); } public static function clear_epoch_cache(): void { wp_cache_delete( 'abientot_import_preview_epoch', 'options' ); wp_cache_delete( 'notoptions', 'options' ); wp_cache_delete( 'alloptions', 'options' ); } }