uuid = $run->uuid ? $run->uuid : wp_generate_uuid4(); $run->created_at = $now; $run->updated_at = $now; $wpdb->insert( // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. $this->table(), array( 'uuid' => $run->uuid, 'relation' => $run->relation, 'parent_run_id' => $run->parent_run_id, 'status' => $run->status, 'operation_id' => $run->operation_id, 'operation_params' => Json::encode( $run->operation_params ), 'cohort_spec' => Json::encode( $run->cohort_spec ), 'cohort_hash' => $run->cohort_hash, 'fingerprint_fields' => Json::encode( array_values( $run->fingerprint_fields ) ), 'fp_version' => $run->fp_version, 'summary' => Json::encode( $run->summary ), 'chunk_size' => $run->chunk_size, 'lanes' => $run->lanes, 'created_by' => $run->created_by, 'created_at' => $now, 'updated_at' => $now, ) ); $run->id = (int) $wpdb->insert_id; // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return $run; } /** * Loads a run. * * @param int $id Id. * @return Run|null */ public function get( $id ) { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $this->table(), $id ), ARRAY_A ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return $row ? Run::from_row( $row ) : null; } /** * Loads by uuid. * * @param string $uuid Uuid. * @return Run|null */ public function get_by_uuid( $uuid ) { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE uuid = %s', $this->table(), $uuid ), ARRAY_A ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return $row ? Run::from_row( $row ) : null; } /** * Current status only (cheap poll). * * @param int $id Id. * @return string|null */ public function status( $id ) { global $wpdb; $status = $wpdb->get_var( $wpdb->prepare( 'SELECT status FROM %i WHERE id = %d', $this->table(), $id ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return null === $status ? null : (string) $status; } /** * Updates columns. * * @param int $id Id. * @param array $fields Column => value (arrays are JSON-encoded). * @return bool */ public function update( $id, array $fields ) { global $wpdb; foreach ( $fields as $key => $value ) { if ( is_array( $value ) ) { $fields[ $key ] = Json::encode( $value ); } } $fields['updated_at'] = current_time( 'mysql', true ); return false !== $wpdb->update( $this->table(), $fields, array( 'id' => (int) $id ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. } /** * Compare-and-set status transition. * * @param int $id Id. * @param string|string[] $from Allowed current statuses. * @param string $to New status. * @param array $extra Extra columns to set with the transition. * @return bool Whether exactly one row changed. */ public function cas_status( $id, $from, $to, array $extra = array() ) { global $wpdb; $from = (array) $from; $set = array( 'status = %s', 'updated_at = %s' ); $vals = array( $to, current_time( 'mysql', true ) ); foreach ( $extra as $column => $value ) { $set[] = sanitize_key( $column ) . ' = %s'; $vals[] = is_array( $value ) ? Json::encode( $value ) : $value; } $placeholders = implode( ',', array_fill( 0, count( $from ), '%s' ) ); $vals[] = (int) $id; $vals = array_merge( $vals, $from ); $sql = "UPDATE {$this->table()} SET " . implode( ', ', $set ) . " WHERE id = %d AND status IN ({$placeholders})"; $updated = $wpdb->query( $wpdb->prepare( $sql, $vals ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return 1 === (int) $updated; } /** * Merges values into the JSON summary (read-modify-write; callers serialise via the preview chain). * * @param int $id Id. * @param array $patch Keys to set. * @return array New summary. */ public function patch_summary( $id, array $patch ) { $run = $this->get( $id ); if ( ! $run ) { return array(); } $summary = array_merge( $run->summary, $patch ); $this->update( $id, array( 'summary' => $summary ) ); return $summary; } /** * Lists runs. * * @param array $filters status[], operation, relation, from, to, finished_from, finished_to (exclusive), parent_run_id. * @param int $page Page. * @param int $per_page Per page. * @return array{runs: Run[], total: int} */ public function list( array $filters = array(), $page = 1, $per_page = 20 ) { global $wpdb; $where = array( '1=1' ); $vals = array(); if ( ! empty( $filters['status'] ) ) { $statuses = (array) $filters['status']; $where[] = 'status IN (' . implode( ',', array_fill( 0, count( $statuses ), '%s' ) ) . ')'; $vals = array_merge( $vals, $statuses ); } if ( ! empty( $filters['operation'] ) ) { $where[] = 'operation_id = %s'; $vals[] = $filters['operation']; } if ( ! empty( $filters['relation'] ) ) { $where[] = 'relation = %s'; $vals[] = $filters['relation']; } if ( ! empty( $filters['parent_run_id'] ) ) { $where[] = 'parent_run_id = %d'; $vals[] = (int) $filters['parent_run_id']; } if ( ! empty( $filters['from'] ) ) { $where[] = 'created_at >= %s'; $vals[] = $filters['from']; } if ( ! empty( $filters['to'] ) ) { $where[] = 'created_at <= %s'; $vals[] = $filters['to']; } if ( ! empty( $filters['finished_from'] ) ) { $where[] = 'finished_at >= %s'; $vals[] = $filters['finished_from']; } if ( ! empty( $filters['finished_to'] ) ) { $where[] = 'finished_at < %s'; $vals[] = $filters['finished_to']; } $where_sql = implode( ' AND ', $where ); $offset = max( 0, ( (int) $page - 1 ) * (int) $per_page ); $count_sql = "SELECT COUNT(*) FROM {$this->table()} WHERE {$where_sql}"; $list_sql = "SELECT * FROM {$this->table()} WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d"; $total = (int) ( $vals ? $wpdb->get_var( $wpdb->prepare( $count_sql, $vals ) ) : $wpdb->get_var( $count_sql ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. $rows = $wpdb->get_results( $wpdb->prepare( $list_sql, array_merge( $vals, array( (int) $per_page, $offset ) ) ), ARRAY_A ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return array( 'runs' => array_map( array( Run::class, 'from_row' ), $rows ? $rows : array() ), 'total' => $total, ); } /** * Deletes a run and its items/events. * * @param int $id Id. * @return void */ public function delete( $id ) { global $wpdb; $wpdb->delete( Schema::table( 'run_items' ), array( 'run_id' => (int) $id ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. $wpdb->delete( Schema::table( 'run_events' ), array( 'run_id' => (int) $id ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. $wpdb->delete( $this->table(), array( 'id' => (int) $id ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. } /** * Appends an event. * * @param int $run_id Run. * @param string $code Code. * @param string $message Message. * @param array $context Context. * @param string $level info|warn|error. * @return void */ public function event( $run_id, $code, $message = '', array $context = array(), $level = 'info' ) { global $wpdb; $wpdb->insert( // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. Schema::table( 'run_events' ), array( 'run_id' => (int) $run_id, 'level' => $level, 'code' => $code, 'message' => $message, 'context' => Json::encode( $context ), 'created_at' => current_time( 'mysql', true ), ) ); } }