Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,17 @@ public static enum Status {
LICENSE_REQUIRED(16),

SUCCESS_WITH_WARNINGS(17),
FAILED_INTEGRITY_CHECK(18);
FAILED_INTEGRITY_CHECK(18),

/**
* Synthetic status for bundles that have been pushed with a future publish date but have
* not yet been picked up by {@code PublisherQueueJob} — i.e. they exist in
* {@code publishing_queue} (with a non-null {@code publish_date}) but have no row in
* {@code publishing_queue_audit} yet. These rows are synthesized at read time by the
* v1 publishing API and are NEVER persisted, so the code is a negative sentinel that can
* never collide with a real {@code publishing_queue_audit.status} value (always 1-18).
*/
SCHEDULED(-1);

private int code;
private Status(int code) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,32 @@ public static PublisherAPI getInstance(){
*/
public abstract List<Map<String,Object>> getQueueBundleIds(int limit, int offest) throws DotPublisherException ;

/**
* Counts distinct bundles in the "scheduled" tier: bundles pushed with a future publish date
* that the {@code PublisherQueueJob} cron has not picked up yet (a row exists in
* {@code publishing_queue} with a non-null {@code publish_date} but no row in
* {@code publishing_queue_audit}). Drafts (null publish_date) are intentionally excluded.
*
* @param filter case-insensitive partial match on bundle id and bundle name, or null/empty for all
* @return number of scheduled bundles matching the filter
* @throws DotPublisherException if any error occurs
*/
public abstract Integer countScheduledBundleIds(String filter) throws DotPublisherException;

/**
* Returns a paginated page of the "scheduled" tier (see {@link #countScheduledBundleIds(String)}),
* one row per bundle ordered by publish date ascending. Each row map contains the keys:
* {@code bundle_id}, {@code bundle_name}, {@code filter_key}, {@code publish_date},
* {@code entered_date}, {@code asset_count} and {@code environment_count}.
*
* @param limit max rows for the page
* @param offset row offset of the page
* @param filter case-insensitive partial match on bundle id and bundle name, or null/empty for all
* @return list of scheduled bundle row maps
* @throws DotPublisherException if any error occurs
*/
public abstract List<Map<String,Object>> getScheduledBundleIds(int limit, int offset, String filter) throws DotPublisherException;

/**
* Get the queue bundles to process
* @return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.dotmarketing.cms.factories.PublicCompanyFactory;
import com.dotmarketing.common.db.DotConnect;
import com.dotmarketing.common.db.Params;
import com.dotmarketing.common.util.SQLUtil;
import com.dotmarketing.db.HibernateUtil;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotHibernateException;
Expand Down Expand Up @@ -637,6 +638,92 @@ public List<Map<String,Object>> getQueueBundleIds(final int limit, final int off
}
}

/**
* Shared FROM/WHERE that defines the "scheduled" tier surfaced by the v1 publishing API:
* bundles pushed with a future publish date that {@link PublisherQueueJob} has not picked up
* yet. They exist in {@code publishing_queue} (non-null {@code publish_date}) but have NO row
* in {@code publishing_queue_audit}. The {@code publish_date IS NOT NULL} guard intentionally
* excludes unpushed/draft bundles (which also lack an audit row) — same criterion the legacy
* Queue tab used via {@link #getQueueBundleIds(int, int)}.
*/
private static final String SCHEDULED_BUNDLES_FROM_WHERE =
"FROM publishing_queue pq " +
"JOIN publishing_bundle pb ON pq.bundle_id = pb.id " +
"WHERE pq.publish_date IS NOT NULL " +
"AND NOT EXISTS (SELECT 1 FROM publishing_queue_audit a WHERE a.bundle_id = pq.bundle_id) ";

private static final String SCHEDULED_BUNDLES_FILTER =
"AND (LOWER(pq.bundle_id) LIKE ? OR LOWER(pb.name) LIKE ?) ";

@CloseDBIfOpened
@Override
public Integer countScheduledBundleIds(final String filter) throws DotPublisherException {
try {
final boolean hasFilter = UtilMethods.isSet(filter);
final StringBuilder sql = new StringBuilder(
"SELECT COUNT(*) AS bundle_count FROM (SELECT pq.bundle_id ")
.append(SCHEDULED_BUNDLES_FROM_WHERE);
if (hasFilter) {
sql.append(SCHEDULED_BUNDLES_FILTER);
}
sql.append("GROUP BY pq.bundle_id) scheduled");

final DotConnect dc = new DotConnect();
dc.setSQL(sql.toString());
if (hasFilter) {
final String likeParam = "%" + SQLUtil.sanitizeParameter(filter).toLowerCase() + "%";
dc.addParam(likeParam);
dc.addParam(likeParam);
}
return Integer.parseInt(dc.loadObjectResults().get(0).get("bundle_count").toString());
} catch (Exception e) {
Logger.error(PublisherAPIImpl.class, e.getMessage(), e);
throw new DotPublisherException("Unable to count scheduled bundles: " + e.getMessage(), e);
}
}

@CloseDBIfOpened
@Override
public List<Map<String, Object>> getScheduledBundleIds(final int limit, final int offset,
final String filter) throws DotPublisherException {
try {
final boolean hasFilter = UtilMethods.isSet(filter);
// One row per bundle. asset_count = queue rows (one per asset); environment_count is a
// correlated subquery, NOT a join, so it does not multiply the asset count.
final StringBuilder sql = new StringBuilder(
"SELECT pq.bundle_id AS bundle_id, " +
"MAX(pb.name) AS bundle_name, " +
"MAX(pb.filter_key) AS filter_key, " +
// publishing_queue.publish_date is the authoritative scheduled time: it is what
// publishBundleAssets() sets and what PublisherQueueJob reads. publishing_bundle's
// publish_date can be stale/null, so the queue value must win.
"COALESCE(MAX(pq.publish_date), MAX(pb.publish_date)) AS publish_date, " +
"COALESCE(MAX(pq.entered_date), MAX(pq.publish_date), MAX(pb.publish_date)) AS entered_date, " +
"COUNT(*) AS asset_count, " +
"(SELECT COUNT(*) FROM publishing_bundle_environment e WHERE e.bundle_id = pq.bundle_id) " +
"AS environment_count ")
.append(SCHEDULED_BUNDLES_FROM_WHERE);
if (hasFilter) {
sql.append(SCHEDULED_BUNDLES_FILTER);
}
sql.append("GROUP BY pq.bundle_id ORDER BY publish_date ASC");

final DotConnect dc = new DotConnect();
dc.setSQL(sql.toString());
if (hasFilter) {
final String likeParam = "%" + SQLUtil.sanitizeParameter(filter).toLowerCase() + "%";
dc.addParam(likeParam);
dc.addParam(likeParam);
}
dc.setMaxRows(limit);
dc.setStartRow(offset);
return dc.loadObjectResults();
} catch (Exception e) {
Logger.error(PublisherAPIImpl.class, e.getMessage(), e);
throw new DotPublisherException("Unable to get scheduled bundles: " + e.getMessage(), e);
}
}

private String SQLGETBUNDLESTOPROCESS =
"select distinct(p.bundle_id) as bundle_id, " +
"publish_date, operation, a.status "+
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import org.immutables.value.Value;

import java.time.Instant;
import java.util.List;

/**
Expand Down Expand Up @@ -131,4 +132,19 @@ public interface AbstractPublishingJobDetailView {
)
int numTries();

/**
* Scheduled execution time for a bundle in {@code SCHEDULED} status — the future
* {@code publishDate} it was pushed with, before the publisher cron picks it up. Null for
* every other status.
*
* @return Scheduled publish date/time, or null when the bundle is not in SCHEDULED status
*/
@Schema(
description = "Scheduled execution time (future publishDate) for SCHEDULED bundles; "
+ "null for all other statuses",
example = "2026-03-15T14:30:00Z"
)
@Nullable
Instant scheduledPublishDate();

}
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,20 @@ public interface AbstractPublishingJobView {
)
int numTries();

/**
* Scheduled execution time for bundles in the {@code SCHEDULED} status — the future
* {@code publishDate} the bundle was pushed with, before the publisher cron picks it up.
* Null for every other status (the bundle is already past the scheduling phase). Distinct
* from {@link #createDate()}, which is when the bundle entered the queue.
*
* @return Scheduled publish date/time, or null when the bundle is not in SCHEDULED status
*/
@Schema(
description = "Scheduled execution time (future publishDate) for SCHEDULED bundles; "
+ "null for all other statuses",
example = "2026-03-15T14:30:00Z"
)
@Nullable
Instant scheduledPublishDate();

}
Loading
Loading