From 75e9a0eb1591153dcdefced6a804293719732567 Mon Sep 17 00:00:00 2001 From: Malloo <26630797+MA2153@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:31:34 +0300 Subject: [PATCH 1/2] fix(loader): seek taxonomy-filtered listings via a denormalized pivot Denormalize the filter+sort columns onto content_taxonomies, mirror the ec_* sort indexes onto the pivot, and drive taxonomy-filtered listings from the pivot with an authoritative ec_* re-check. Fixes full-collection scans (~75k D1 rows read for a one-row page) on selective terms (#1834). Co-Authored-By: Claude Opus 4.8 --- .changeset/taxonomy-filter-pivot-seek.md | 5 + .../051_content_taxonomies_denorm.ts | 120 +++++ .../core/src/database/migrations/runner.ts | 2 + .../core/src/database/repositories/content.ts | 48 +- .../src/database/repositories/taxonomy.ts | 65 ++- packages/core/src/database/types.ts | 21 + packages/core/src/loader.ts | 293 ++++++++++- .../integration/database/migrations.test.ts | 1 + .../loader-taxonomy-pivot-plan.test.ts | 145 +++++ .../tests/unit/loader-byline-filter.test.ts | 8 +- .../tests/unit/loader-field-filters.test.ts | 12 +- .../tests/unit/loader-taxonomy-filter.test.ts | 10 +- .../tests/unit/loader-taxonomy-pivot.test.ts | 498 ++++++++++++++++++ 13 files changed, 1202 insertions(+), 26 deletions(-) create mode 100644 .changeset/taxonomy-filter-pivot-seek.md create mode 100644 packages/core/src/database/migrations/051_content_taxonomies_denorm.ts create mode 100644 packages/core/tests/integration/loader-taxonomy-pivot-plan.test.ts create mode 100644 packages/core/tests/unit/loader-taxonomy-pivot.test.ts diff --git a/.changeset/taxonomy-filter-pivot-seek.md b/.changeset/taxonomy-filter-pivot-seek.md new file mode 100644 index 0000000000..b4c6ff20e0 --- /dev/null +++ b/.changeset/taxonomy-filter-pivot-seek.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fixes taxonomy-filtered collection listings reading the entire collection on SQLite/D1 when the term is selective. Such listings now seek the matching entries directly, cutting D1 rows-read from tens of thousands to the page size. diff --git a/packages/core/src/database/migrations/051_content_taxonomies_denorm.ts b/packages/core/src/database/migrations/051_content_taxonomies_denorm.ts new file mode 100644 index 0000000000..305d5126ed --- /dev/null +++ b/packages/core/src/database/migrations/051_content_taxonomies_denorm.ts @@ -0,0 +1,120 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +import { columnExists, listTablesLike } from "../dialect-helpers.js"; +import { validateIdentifier } from "../validate.js"; + +/** + * Denormalize the filter + sort columns from `ec_*` onto `content_taxonomies` + * and mirror `ec_*`'s composite sort indexes onto the pivot (#1834). + * + * A taxonomy-filtered listing used to apply the term filter as a correlated + * `EXISTS` on `SELECT * FROM ec_ … ORDER BY … LIMIT ?`. The pivot + * carried only `(collection, entry_id, taxonomy_id)`, so the filter + * (`deleted_at`, `status`, `locale`) and the sort (`published_at`/`created_at`) + * could only be evaluated on the `ec_*` row. On stats-blind SQLite/D1 a + * selective term never fills the `LIMIT`, so the query walked the whole + * collection (~75k D1 rows read for a one-row page). + * + * Copying those columns onto the pivot and indexing them keyed by + * `(taxonomy_id, collection, …)` lets the loader seek the term and walk a + * sort-ordered pivot index, short-circuiting on `LIMIT`, then touch `ec_*` only + * by primary key to hydrate the page. + * + * The columns are advisory (D1 has no transactions; the write-path re-stamp is + * non-atomic), so the read path re-checks the real predicates on the joined + * `ec_*` row — see loader.ts. `updated_at` is deliberately not denormalized: it + * moves on every edit and is seldom a public sort, so its write cost outweighs + * its read value; `updated_at`-sorted taxonomy listings temp-sort instead. + * + * Order matters: add the columns, backfill, THEN build the indexes so each + * index is built once over populated data rather than maintained through the + * backfill `UPDATE`. + * + * Forward-only. + */ + +const DENORM_COLUMNS = [ + "status", + "scheduled_at", + "deleted_at", + "locale", + "published_at", + "created_at", +] as const; + +// `entry_id DESC` (not ASC) matters: the listing orders ` DESC, entry_id +// DESC` (the common case), and a DESC tiebreaker lets SQLite satisfy the whole +// ORDER BY from the index — no temp B-tree, clean early-`LIMIT`. An ASC +// tiebreaker forces `USE TEMP B-TREE FOR LAST TERM OF ORDER BY`, which buffers +// a whole equal-`sortval` block (e.g. a bulk import sharing one timestamp) +// before emitting. `DESC` also serves the rarer ASC listing via a backward +// index scan. Mirrors `ec_*`'s `(deleted_at, [locale,] DESC, id DESC)`. +const INDEXES: { name: string; columns: string }[] = [ + { + name: "idx_content_taxonomies_pub", + columns: "taxonomy_id, collection, deleted_at, published_at DESC, entry_id DESC", + }, + { + name: "idx_content_taxonomies_crt", + columns: "taxonomy_id, collection, deleted_at, created_at DESC, entry_id DESC", + }, + { + name: "idx_content_taxonomies_loc_pub", + columns: "taxonomy_id, collection, deleted_at, locale, published_at DESC, entry_id DESC", + }, + { + name: "idx_content_taxonomies_loc_crt", + columns: "taxonomy_id, collection, deleted_at, locale, created_at DESC, entry_id DESC", + }, +]; + +export async function up(db: Kysely): Promise { + // 1. Add the six denormalized columns (all nullable, no default). Guarded so + // a partial apply — or a re-run after a mid-migration failure — can retry + // (`ALTER TABLE ADD COLUMN` is not itself idempotent on either dialect). + for (const column of DENORM_COLUMNS) { + if (await columnExists(db, "content_taxonomies", column)) continue; + await sql` + ALTER TABLE content_taxonomies ADD COLUMN ${sql.ref(column)} TEXT + `.execute(db); + } + + // 2. Backfill from each content table by primary key. Drive from the actual + // `ec_*` tables (not `_emdash_collections`) so collections whose table was + // dropped are skipped naturally; the pivot's `collection` value is the + // slug, i.e. the table name without its `ec_` prefix. + const tableNames = await listTablesLike(db, "ec_%"); + for (const tableName of tableNames) { + validateIdentifier(tableName, "content table name"); + const slug = tableName.slice("ec_".length); + await sql` + UPDATE content_taxonomies + SET (status, scheduled_at, deleted_at, locale, published_at, created_at) = ( + SELECT status, scheduled_at, deleted_at, locale, published_at, created_at + FROM ${sql.ref(tableName)} + WHERE ${sql.ref(tableName)}.id = content_taxonomies.entry_id + ) + WHERE collection = ${slug} + `.execute(db); + } + + // 3. Build the composite sort indexes over the now-populated columns. + for (const index of INDEXES) { + await sql` + CREATE INDEX IF NOT EXISTS ${sql.ref(index.name)} + ON content_taxonomies (${sql.raw(index.columns)}) + `.execute(db); + } +} + +export async function down(db: Kysely): Promise { + for (const index of INDEXES) { + await sql`DROP INDEX IF EXISTS ${sql.ref(index.name)}`.execute(db); + } + for (const column of DENORM_COLUMNS) { + await sql` + ALTER TABLE content_taxonomies DROP COLUMN ${sql.ref(column)} + `.execute(db); + } +} diff --git a/packages/core/src/database/migrations/runner.ts b/packages/core/src/database/migrations/runner.ts index e2f0e63d58..ca2020144e 100644 --- a/packages/core/src/database/migrations/runner.ts +++ b/packages/core/src/database/migrations/runner.ts @@ -53,6 +53,7 @@ import * as m047 from "./047_restore_taxonomy_parent_index.js"; import * as m048 from "./048_restore_content_taxonomies_term_index.js"; import * as m049 from "./049_taxonomies_name_locale_index.js"; import * as m050 from "./050_media_usage_index_status.js"; +import * as m051 from "./051_content_taxonomies_denorm.js"; const MIGRATIONS: Readonly> = Object.freeze({ "001_initial": m001, @@ -104,6 +105,7 @@ const MIGRATIONS: Readonly> = Object.freeze({ "048_restore_content_taxonomies_term_index": m048, "049_taxonomies_name_locale_index": m049, "050_media_usage_index_status": m050, + "051_content_taxonomies_denorm": m051, }); /** Total number of registered migrations. Exported for use in tests. */ diff --git a/packages/core/src/database/repositories/content.ts b/packages/core/src/database/repositories/content.ts index 7e0ded0877..a0b17a6014 100644 --- a/packages/core/src/database/repositories/content.ts +++ b/packages/core/src/database/repositories/content.ts @@ -637,6 +637,18 @@ export class ContentRepository { .where("deleted_at" as never, "is", null) .execute(); + // Re-stamp the taxonomy pivot only when a denormalized column actually + // moved (status/publishedAt/scheduledAt). A plain content edit bumps + // `updated_at` — which is not denormalized — so it needs no pivot write, + // keeping the common edit path free of taxonomy write amplification. + if ( + input.status !== undefined || + input.publishedAt !== undefined || + input.scheduledAt !== undefined + ) { + await this.restampEntryPivot(type, id); + } + invalidateCollectionCache(type); const updated = await this.findById(type, id); @@ -662,7 +674,10 @@ export class ContentRepository { `.execute(this.db); const changed = (result.numAffectedRows ?? 0n) > 0n; - if (changed) invalidateCollectionCache(type); + if (changed) { + await this.restampEntryPivot(type, id); + invalidateCollectionCache(type); + } return changed; } @@ -683,10 +698,36 @@ export class ContentRepository { const restored = result.rows[0]; if (!restored) return null; + await this.restampEntryPivot(type, id); invalidateCollectionCache(type); return this.mapRow(type, restored); } + /** + * Re-stamp the denormalized filter + sort columns on every + * `content_taxonomies` pivot row for an entry from its authoritative `ec_*` + * row (migration 051). Called after any mutation that moves one of those + * columns so a taxonomy-filtered listing can seek the entry directly. + * + * A single correlated `UPDATE` reads the post-mutation values from `ec_*`, so + * the pivot converges to the authoritative row. This is NOT atomic with the + * `ec_*` mutation on D1 (no transactions), which is why the read path + * re-checks the real predicates on the joined `ec_*` row. Untagged entries + * have no pivot rows, so the statement is a cheap no-op for them. + */ + private async restampEntryPivot(type: string, id: string): Promise { + const tableName = getTableName(type); + await sql` + UPDATE content_taxonomies + SET (status, scheduled_at, deleted_at, locale, published_at, created_at) = ( + SELECT status, scheduled_at, deleted_at, locale, published_at, created_at + FROM ${sql.ref(tableName)} + WHERE ${sql.ref(tableName)}.id = ${id} + ) + WHERE collection = ${type} AND entry_id = ${id} + `.execute(this.db); + } + /** * Permanently delete content (cannot be undone) */ @@ -996,6 +1037,7 @@ export class ContentRepository { AND deleted_at IS NULL `.execute(this.db); + await this.restampEntryPivot(type, id); invalidateCollectionCache(type); const updated = await this.findById(type, id); @@ -1035,6 +1077,7 @@ export class ContentRepository { AND deleted_at IS NULL `.execute(this.db); + await this.restampEntryPivot(type, id); invalidateCollectionCache(type); const updated = await this.findById(type, id); @@ -1339,6 +1382,8 @@ export class ContentRepository { } publishCommitted = true; + await this.restampEntryPivot(type, id); + const updated = await this.findById(type, id); if (!updated) { throw new Error("Content not found"); @@ -1427,6 +1472,7 @@ export class ContentRepository { AND deleted_at IS NULL `.execute(this.db); + await this.restampEntryPivot(type, id); invalidateCollectionCache(type); const updated = await this.findById(type, id); diff --git a/packages/core/src/database/repositories/taxonomy.ts b/packages/core/src/database/repositories/taxonomy.ts index 89f2973d15..2e22eff4b8 100644 --- a/packages/core/src/database/repositories/taxonomy.ts +++ b/packages/core/src/database/repositories/taxonomy.ts @@ -1,8 +1,33 @@ -import type { Kysely, Selectable } from "kysely"; +import { sql, type Kysely, type Selectable } from "kysely"; import { ulid } from "ulidx"; import { invalidateTaxonomyObjectCache } from "../../object-cache/index.js"; -import type { Database, TaxonomyTable, ContentTaxonomyTable } from "../types.js"; +import { isMissingTableError } from "../../utils/db-errors.js"; +import type { Database, TaxonomyTable } from "../types.js"; +import { validateIdentifier } from "../validate.js"; + +/** + * Filter + sort columns denormalized from an entry's `ec_*` row onto its + * `content_taxonomies` pivot rows (migration 051). Stamped at insert time so a + * newly-tagged entry is immediately seekable by a taxonomy-filtered listing. + */ +interface PivotDenorm { + status: string | null; + scheduled_at: string | null; + deleted_at: string | null; + locale: string | null; + published_at: string | null; + created_at: string | null; +} + +const EMPTY_DENORM: PivotDenorm = { + status: null, + scheduled_at: null, + deleted_at: null, + locale: null, + published_at: null, + created_at: null, +}; export interface Taxonomy { id: string; @@ -251,14 +276,10 @@ export class TaxonomyRepository { const group = await this.resolveTranslationGroup(taxonomyId); if (!group) return; - const row: ContentTaxonomyTable = { - collection, - entry_id: entryId, - taxonomy_id: group, - }; + const denorm = await this.fetchEntryDenorm(collection, entryId); await this.db .insertInto("content_taxonomies") - .values(row) + .values({ collection, entry_id: entryId, taxonomy_id: group, ...denorm }) .onConflict((oc) => oc.doNothing()) .execute(); invalidateTaxonomyObjectCache(); @@ -342,6 +363,7 @@ export class TaxonomyRepository { const toAdd = [...newGroups].filter((g) => !currentGroups.has(g)); if (toAdd.length > 0) { + const denorm = await this.fetchEntryDenorm(collection, entryId); await this.db .insertInto("content_taxonomies") .values( @@ -349,6 +371,7 @@ export class TaxonomyRepository { collection, entry_id: entryId, taxonomy_id, + ...denorm, })), ) .onConflict((oc) => oc.doNothing()) @@ -387,6 +410,9 @@ export class TaxonomyRepository { .execute(); if (rows.length === 0) return; + // Stamp the TARGET entry's current values — the copy inherits the source's + // term memberships but the target's own status/dates/locale. + const denorm = await this.fetchEntryDenorm(collection, targetEntryId); await this.db .insertInto("content_taxonomies") .values( @@ -394,6 +420,7 @@ export class TaxonomyRepository { collection, entry_id: targetEntryId, taxonomy_id: r.taxonomy_id, + ...denorm, })), ) .onConflict((oc) => oc.doNothing()) @@ -401,6 +428,28 @@ export class TaxonomyRepository { invalidateTaxonomyObjectCache(); } + /** + * Read the denormalized filter + sort columns from an entry's `ec_*` row so + * they can be stamped onto new pivot rows (migration 051). A missing table or + * missing row yields all-nulls: the pivot columns are advisory, and the + * listing read path re-checks the authoritative `ec_*` row regardless. + */ + private async fetchEntryDenorm(collection: string, entryId: string): Promise { + validateIdentifier(collection, "collection type"); + const tableName = `ec_${collection}`; + try { + const result = await sql` + SELECT status, scheduled_at, deleted_at, locale, published_at, created_at + FROM ${sql.ref(tableName)} + WHERE id = ${entryId} + `.execute(this.db); + return result.rows[0] ?? EMPTY_DENORM; + } catch (error) { + if (isMissingTableError(error)) return EMPTY_DENORM; + throw error; + } + } + /** * Count content entries that use any translation of this term. Accepts * either a term id or a translation_group — we normalise to the group. diff --git a/packages/core/src/database/types.ts b/packages/core/src/database/types.ts index 9b5eeac3f4..b6023ed022 100644 --- a/packages/core/src/database/types.ts +++ b/packages/core/src/database/types.ts @@ -28,6 +28,27 @@ export interface ContentTaxonomyTable { collection: string; // e.g., 'posts' entry_id: string; // ID in the ec_* table taxonomy_id: string; // stores taxonomies.translation_group (locale-agnostic) + // Denormalized filter + sort columns mirrored from the entry's ec_* row + // (migration 051). They let a taxonomy-filtered listing seek the matching + // entries directly on the pivot instead of scanning the whole collection. + // + // ADVISORY, not authoritative: D1 has no transactions, so the write-path + // re-stamp (ContentRepository / TaxonomyRepository) is a separate statement + // from the ec_* mutation and can be transiently stale. The read path narrows + // the candidate set with these columns but re-checks the real filter + // predicates on the joined ec_* row. `updated_at` is deliberately NOT + // denormalized — it moves on every edit, so denormalizing it would force a + // pivot re-stamp on the common edit path for little read value. + // + // Nullable/Generated so inserts that predate a re-stamp (or the migration + // backfill) leave them NULL; every insert site in the repositories stamps + // them explicitly. + status: Generated; + scheduled_at: Generated; + deleted_at: Generated; + locale: Generated; + published_at: Generated; + created_at: Generated; } export interface TaxonomyDefTable { diff --git a/packages/core/src/loader.ts b/packages/core/src/loader.ts index 47e93536eb..d85391ea2b 100644 --- a/packages/core/src/loader.ts +++ b/packages/core/src/loader.ts @@ -646,6 +646,263 @@ function buildFieldConditions( return conditions; } +/** + * Resolve a taxonomy filter (`name` + one or more `slug`s, optionally scoped to + * `locale`) to the set of `translation_group`s the pivot stores in + * `content_taxonomies.taxonomy_id`. Exact terms only — no subtree expansion. + * + * Mirrors the meaning of the old EXISTS join (`t.name = ? AND t.slug IN (?) + * [AND t.locale = ?]`): a pivot row matches when its group has a term with that + * name/slug in the active locale. Resolving to explicit values (rather than an + * `IN (subquery)`) keeps the single-term case a plain equality on the pivot + * index, which is what gives the clean early-`LIMIT` seek. + */ +async function resolveTermGroups( + db: Kysely, + name: string, + slugs: string[], + locale: string | undefined, +): Promise { + let query = db + .selectFrom("taxonomies") + .select("translation_group") + .distinct() + .where("name", "=", name) + .where("slug", "in", slugs); + if (locale) query = query.where("locale", "=", locale); + const rows = await query.execute(); + const groups = new Set(); + for (const row of rows) { + if (row.translation_group) groups.add(row.translation_group); + } + return [...groups]; +} + +/** Equality (single) or `IN` (multiple) condition on a pivot group column. */ +function pivotGroupCondition(ref: string, groups: string[]): ReturnType { + if (groups.length === 1) return sql`${sql.ref(ref)} = ${groups[0]}`; + return sql`${sql.ref(ref)} IN (${sql.join(groups.map((g) => sql`${g}`))})`; +} + +/** LIMIT/OFFSET fragment matching the loader's single-table variant. */ +function buildPivotLimitOffset( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- any Kysely instance + db: Kysely, + fetchLimit: number | undefined, + offset: number | undefined, +): ReturnType { + if (fetchLimit != null && offset != null) return sql`LIMIT ${fetchLimit} OFFSET ${offset}`; + if (fetchLimit != null) return sql`LIMIT ${fetchLimit}`; + if (offset != null) { + return isPostgres(db) ? sql`OFFSET ${offset}` : sql`LIMIT -1 OFFSET ${offset}`; + } + return sql``; +} + +/** + * Options for {@link buildTaxonomyPivotQuery}. + * + * Parameterized on the `deletedIsNull` predicate and `status` condition so the + * same builder serves the public live path (`deleted_at IS NULL` + published/ + * scheduled) and, without any schema change, an admin trash (`deleted_at IS NOT + * NULL`) or all-statuses shape. Admin wiring is out of scope today; the + * parameters exist so `ContentRepository` can adopt this if it gains taxonomy + * filtering. + */ +export interface TaxonomyPivotQueryOptions { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- any Kysely instance + db: Kysely; + /** Collection slug (pivot `collection` value). */ + collection: string; + /** Content table name (`ec_`). */ + tableName: string; + /** + * Resolved translation_group sets, one per taxonomy filter. The first drives + * the pivot seek; each additional set becomes a residual `EXISTS` (AND across + * taxonomies). Multiple groups within a set are OR'd (dedup via GROUP BY). + */ + groupSets: string[][]; + orderBy: OrderBySpec | undefined; + cursor: string | undefined; + locale: string | undefined; + /** + * Status shape. A concrete value (`published`/`draft`/…) applies the same + * condition `buildStatusCondition` produces (public: published-or-scheduled). + * `undefined` drops the status filter entirely — the admin all-statuses shape. + */ + status: string | undefined; + /** `true` → `deleted_at IS NULL` (live); `false` → `IS NOT NULL` (trash). */ + deletedIsNull: boolean; + /** Byline translation_groups for an AND'd byline filter, or `null`. */ + bylineGroups: string[] | null; + fetchLimit: number | undefined; + offset: number | undefined; +} + +/** + * Build the pivot-driven taxonomy listing query (#1834). + * + * Drives from a pivot-only CTE (`picked`) that carries the sort column, seeks + * the term on a `(taxonomy_id, collection, deleted_at, [locale,] DESC, + * entry_id DESC)` index, and lets `LIMIT` short-circuit; then joins `ec_*` by primary + * key to hydrate the page **and re-checks the real filter predicates on the + * joined row** — the pivot columns are advisory (non-atomic re-stamp on D1), so + * `ec_*` is authoritative for membership. Ordering stays pivot-driven to keep + * the early-`LIMIT`. + * + * Two shapes: + * - **Indexed sort** (`published_at`/`created_at`, single sort field): the + * pivot index is covering for `(entry_id, sortval)`, so `LIMIT` lives in + * `picked` and short-circuits. + * - **Temp-sort** (`updated_at` or any other field, or multi-field sort): no + * pivot sort index applies, so `picked` collects the tagged candidate set and + * the outer query sorts the joined rows. Bounded to tagged rows — no + * `ec_*` full scan — but no early-`LIMIT`. + */ +export function buildTaxonomyPivotQuery( + opts: TaxonomyPivotQueryOptions, +): ReturnType>> { + const { + db, + collection, + tableName, + groupSets, + orderBy, + cursor, + locale, + status, + deletedIsNull, + bylineGroups, + fetchLimit, + offset, + } = opts; + + const primary = getPrimarySort(orderBy); + const validSortKeys = orderBy + ? Object.keys(orderBy).filter((k) => FIELD_NAME_PATTERN.test(k)) + : []; + const singleSort = validSortKeys.length <= 1; + const isIndexedSort = + singleSort && (primary.field === "published_at" || primary.field === "created_at"); + const dir = primary.direction === "asc" ? sql`ASC` : sql`DESC`; + const cmp = primary.direction === "asc" ? sql.raw(">") : sql.raw("<"); + + const firstGroups = groupSets[0] ?? []; + const restGroups = groupSets.slice(1); + const multiGroup = firstGroups.length > 1; + + // Pivot-local narrowing predicates (advisory — re-checked on `ec_*` below). + // `status === undefined` means an all-statuses shape (admin), so the status + // condition is dropped entirely. + const deletedCt = deletedIsNull ? sql`ct.deleted_at IS NULL` : sql`ct.deleted_at IS NOT NULL`; + const statusCt = + status !== undefined ? sql`AND ${buildStatusCondition(db, status, "ct")}` : sql``; + const localeCt = locale ? sql`AND ct.locale = ${locale}` : sql``; + + // Multi-term AND: one residual pivot-PK EXISTS per additional taxonomy. + const residual = + restGroups.length > 0 + ? sql`${sql.join( + restGroups.map( + (g) => sql`AND EXISTS ( + SELECT 1 FROM content_taxonomies ct2 + WHERE ct2.collection = ${collection} + AND ct2.entry_id = ct.entry_id + AND ${pivotGroupCondition("ct2.taxonomy_id", g)} + )`, + ), + sql` `, + )}` + : sql``; + + // Byline filter keeps its EXISTS, correlated on `ct.entry_id`. + const bylineCt = bylineGroups + ? sql`AND EXISTS ( + SELECT 1 FROM _emdash_content_bylines cb + WHERE cb.collection_slug = ${collection} + AND cb.content_id = ct.entry_id + AND cb.byline_id IN (${sql.join(bylineGroups.map((g) => sql`${g}`))}) + )` + : sql``; + + const firstGroupCond = pivotGroupCondition("ct.taxonomy_id", firstGroups); + const { terms: termsSelect, bylines: bylinesSelect } = foldedHydrationSelects( + db, + collection, + "r", + ); + + // Authoritative re-check on the joined `ec_*` row. + const deletedR = deletedIsNull ? sql`r.deleted_at IS NULL` : sql`r.deleted_at IS NOT NULL`; + const statusR = status !== undefined ? sql`AND ${buildStatusCondition(db, status, "r")}` : sql``; + const localeR = locale ? sql`AND r.locale = ${locale}` : sql``; + + if (isIndexedSort) { + const sortRef = sql.ref(`ct.${primary.field}`); + const sortval = multiGroup ? sql`MAX(${sortRef})` : sortRef; + const groupByClause = multiGroup ? sql`GROUP BY ct.entry_id` : sql``; + + let cursorClause = sql``; + let havingClause = sql``; + if (cursor) { + const { orderValue, id } = decodeCursor(cursor); + const cond = sql`(${sortval} ${cmp} ${orderValue} OR (${sortval} = ${orderValue} AND ct.entry_id ${cmp} ${id}))`; + // A GROUP BY makes `sortval` an aggregate → cursor goes in HAVING. + if (multiGroup) havingClause = sql`HAVING ${cond}`; + else cursorClause = sql`AND ${cond}`; + } + + const limitClause = buildPivotLimitOffset(db, fetchLimit, offset); + + return sql>` + WITH picked AS ( + SELECT ct.entry_id AS entry_id, ${sortval} AS sortval + FROM content_taxonomies ct + WHERE ct.collection = ${collection} + AND ${firstGroupCond} + AND ${deletedCt} + ${statusCt} + ${localeCt} + ${residual} + ${bylineCt} + ${cursorClause} + ${groupByClause} + ${havingClause} + ORDER BY sortval ${dir}, ct.entry_id ${dir} + ${limitClause} + ) + SELECT r.*, ${termsSelect}, ${bylinesSelect} + FROM picked JOIN ${sql.ref(tableName)} AS r ON r.id = picked.entry_id + WHERE ${deletedR} ${statusR} ${localeR} + ORDER BY picked.sortval ${dir}, picked.entry_id ${dir} + `; + } + + // Temp-sort path: seek the term via the pivot, sort the joined candidate set. + const orderByClause = buildOrderByClause(orderBy, "r"); + const cursorCond = cursor ? sql`AND ${buildCursorCondition(cursor, orderBy, "r")}` : sql``; + const limitClause = buildPivotLimitOffset(db, fetchLimit, offset); + return sql>` + WITH picked AS ( + SELECT DISTINCT ct.entry_id AS entry_id + FROM content_taxonomies ct + WHERE ct.collection = ${collection} + AND ${firstGroupCond} + AND ${deletedCt} + ${statusCt} + ${localeCt} + ${residual} + ${bylineCt} + ) + SELECT r.*, ${termsSelect}, ${bylinesSelect} + FROM picked JOIN ${sql.ref(tableName)} AS r ON r.id = picked.entry_id + WHERE ${deletedR} ${statusR} ${localeR} + ${cursorCond} + ${orderByClause} + ${limitClause} + `; +} + /** * Range filter for comparison operators on field values. * Values are compared as strings in the database. This works correctly for @@ -912,7 +1169,41 @@ export function emdashLoader(): LiveLoader 0 && Object.keys(fieldFilters).length === 0) { + // Pivot-drive fast path (#1834): seek the matching entries on the + // denormalized `content_taxonomies` pivot instead of scanning the + // whole collection and probing a taxonomy EXISTS per row. Only the + // taxonomy path is restructured — a taxonomy filter combined with a + // content-field filter falls through to the single-table shape + // below (field predicates live on `ec_*`, not the pivot). A byline + // filter rides along inside the pivot CTE (see the builder). + const groupSets: string[][] = []; + for (const taxFilter of taxonomyFilters) { + const groups = await resolveTermGroups(db, taxFilter.name, taxFilter.slugs, locale); + // A slug that resolves to no term matches nothing; since taxonomy + // filters AND together, one empty set empties the whole result. + if (groups.length === 0) { + return { entries: [], cacheHint: { tags: [type] } }; + } + groupSets.push(groups); + } + + result = await buildTaxonomyPivotQuery({ + db, + collection: type, + tableName, + groupSets, + orderBy, + cursor, + locale, + status, + // Public listings only ever want live content. + deletedIsNull: true, + bylineGroups: bylineFilter ? bylineFilter.groups : null, + fetchLimit, + offset, + }).execute(db); + } else { // Taxonomy and byline filters are applied as correlated // `EXISTS` semi-joins rather than `INNER JOIN ... DISTINCT`. // A join fan-out would force `SELECT DISTINCT table.*`, and diff --git a/packages/core/tests/integration/database/migrations.test.ts b/packages/core/tests/integration/database/migrations.test.ts index f654a75cf3..6380bf5d04 100644 --- a/packages/core/tests/integration/database/migrations.test.ts +++ b/packages/core/tests/integration/database/migrations.test.ts @@ -137,6 +137,7 @@ describe("Database Migrations (Integration)", () => { "048_restore_content_taxonomies_term_index", "049_taxonomies_name_locale_index", "050_media_usage_index_status", + "051_content_taxonomies_denorm", ]; await db.deleteFrom("_emdash_migrations").where("name", "in", trailing).execute(); diff --git a/packages/core/tests/integration/loader-taxonomy-pivot-plan.test.ts b/packages/core/tests/integration/loader-taxonomy-pivot-plan.test.ts new file mode 100644 index 0000000000..5a63a8dcf7 --- /dev/null +++ b/packages/core/tests/integration/loader-taxonomy-pivot-plan.test.ts @@ -0,0 +1,145 @@ +/** + * Query-plan shape of the pivot-driven taxonomy listing (#1834). + * + * On stats-blind SQLite/D1 (no ANALYZE, no `sqlite_stat1`) the old EXISTS shape + * drove the scan from the collection's order index and probed a taxonomy EXISTS + * per row — a full `ec_*` walk for a selective term. The restructure seeks the + * term on a `(taxonomy_id, collection, deleted_at, [locale,] DESC, + * entry_id)` pivot index, lets `LIMIT` short-circuit, and touches `ec_*` only by + * primary key. + * + * This asserts the plan, not the output (output is covered by + * loader-taxonomy-pivot). SQLite-only: `EXPLAIN QUERY PLAN` is a SQLite concern + * and, being stats-blind here, the plan is schema-driven — matching D1 exactly. + */ + +import Database from "better-sqlite3"; +import { Kysely, SqliteDialect } from "kysely"; +import { afterEach, beforeEach, expect, it } from "vitest"; + +import { runMigrations } from "../../src/database/migrations/runner.js"; +import { ContentRepository } from "../../src/database/repositories/content.js"; +import { TaxonomyRepository } from "../../src/database/repositories/taxonomy.js"; +import type { Database as DatabaseSchema } from "../../src/database/types.js"; +import { emdashLoader } from "../../src/loader.js"; +import { runWithContext } from "../../src/request-context.js"; +import { SchemaRegistry } from "../../src/schema/registry.js"; + +interface CapturedQuery { + sql: string; + parameters: readonly unknown[]; +} + +let sqlite: Database.Database; +let db: Kysely; +let captured: CapturedQuery[]; + +beforeEach(async () => { + captured = []; + sqlite = new Database(":memory:"); + db = new Kysely({ + dialect: new SqliteDialect({ database: sqlite }), + log(event) { + if (event.level === "query") { + captured.push({ sql: event.query.sql, parameters: event.query.parameters }); + } + }, + }); + + // Deliberately no ANALYZE: matches D1, which never maintains sqlite_stat1. + await runMigrations(db); + const registry = new SchemaRegistry(db); + await registry.createCollection({ slug: "post", label: "Posts", labelSingular: "Post" }); + await registry.createField("post", { slug: "title", label: "Title", type: "string" }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- schema vs Database type + const anyDb = db as any; + const content = new ContentRepository(anyDb); + const tax = new TaxonomyRepository(anyDb); + const term = await tax.create({ name: "category", slug: "news", label: "News", locale: "en" }); + // A selective term: one tagged entry among many. The plan is stats-blind so + // the ratio is immaterial — the point is that the seek short-circuits. + for (let i = 0; i < 30; i++) { + const post = await content.create({ + type: "post", + slug: `post-${i}`, + data: { title: `Post ${i}` }, + status: "published", + locale: "en", + }); + if (i === 0) await tax.attachToEntry("post", post.id, term.id); + } +}); + +afterEach(async () => { + await db.destroy(); +}); + +/** better-sqlite3 only binds primitives; coerce the JS values Kysely captured. */ +function bindable(p: unknown): unknown { + if (typeof p === "boolean") return p ? 1 : 0; + if (p instanceof Date) return p.toISOString(); + if (p === undefined) return null; + return p; +} + +function explain(query: CapturedQuery): string { + const rows = sqlite + .prepare(`EXPLAIN QUERY PLAN ${query.sql}`) + .all(...query.parameters.map(bindable)) as { detail: string }[]; + return rows.map((r) => r.detail).join("\n"); +} + +/** The pivot-drive query is the one with the `picked` CTE. */ +function pivotQueryPlan(): string { + const query = captured.find((q) => q.sql.includes("picked")); + expect(query, "expected the loader to emit a pivot-drive query").toBeDefined(); + return explain(query!); +} + +async function runLoad(extra: Record): Promise { + captured = []; + const loader = emdashLoader(); + await runWithContext({ editMode: false, db }, () => + loader.loadCollection!({ + filter: { type: "post", where: { category: "news" } as never, limit: 5, ...extra }, + }), + ); +} + +it("seeks the term via idx_content_taxonomies_pub for a published_at sort", async () => { + await runLoad({ orderBy: { published_at: "desc" } }); + const plan = pivotQueryPlan(); + expect(plan).toContain("idx_content_taxonomies_pub"); + // No full scan of the content table — it is reached only by primary key. + expect(plan).not.toContain("SCAN r"); + // The `entry_id DESC` tiebreaker lets the index satisfy the whole ORDER BY, + // so the LIMIT short-circuits without buffering an equal-sortval block. + expect(plan).not.toContain("TEMP B-TREE"); +}); + +it("seeks via idx_content_taxonomies_crt for the default created_at sort", async () => { + await runLoad({}); + const plan = pivotQueryPlan(); + expect(plan).toContain("idx_content_taxonomies_crt"); + expect(plan).not.toContain("SCAN r"); + expect(plan).not.toContain("TEMP B-TREE"); +}); + +it("uses the locale-variant index (loc_pub) when locale-filtered + published_at", async () => { + await runLoad({ orderBy: { published_at: "desc" }, locale: "en" }); + const plan = pivotQueryPlan(); + expect(plan).toContain("idx_content_taxonomies_loc_pub"); + expect(plan).not.toContain("SCAN r"); + expect(plan).not.toContain("TEMP B-TREE"); +}); + +it("updated_at sort seeks the term via the pivot and does not full-scan the content table", async () => { + await runLoad({ orderBy: { updated_at: "desc" } }); + const plan = pivotQueryPlan(); + // A pivot index seek on taxonomy_id (any composite is prefixed by it), not a + // full pivot scan and not a full ec_* scan. + expect(plan).toContain("idx_content_taxonomies"); + expect(plan).not.toContain("SCAN ct"); + expect(plan).not.toContain("SCAN r"); +}); diff --git a/packages/core/tests/unit/loader-byline-filter.test.ts b/packages/core/tests/unit/loader-byline-filter.test.ts index 32011407d3..edbacc2ea2 100644 --- a/packages/core/tests/unit/loader-byline-filter.test.ts +++ b/packages/core/tests/unit/loader-byline-filter.test.ts @@ -2,6 +2,7 @@ import type { Kysely } from "kysely"; import { it, expect, beforeEach, afterEach } from "vitest"; import { handleContentCreate } from "../../src/api/index.js"; +import { TaxonomyRepository } from "../../src/database/repositories/taxonomy.js"; import type { Database } from "../../src/database/types.js"; import { emdashLoader } from "../../src/loader.js"; import { runWithContext } from "../../src/request-context.js"; @@ -163,10 +164,9 @@ describeEachDialect("Loader byline credit filter", (dialectName: DialectName) => const b = await createPost("Bob Other"); await credit(a.id, "byline_bob", 0); await credit(b.id, "byline_bob", 0); - await db - .insertInto("content_taxonomies" as never) - .values({ collection: "post", entry_id: a.id, taxonomy_id: "tax_cat_news" } as never) - .execute(); + // Attach through the repository so the pivot row carries the denormalized + // filter/sort columns the pivot-drive listing seeks on (migration 051). + await new TaxonomyRepository(db).attachToEntry("post", a.id, "tax_cat_news"); const result = await load({ byline: "byline_bob", category: "news" }); diff --git a/packages/core/tests/unit/loader-field-filters.test.ts b/packages/core/tests/unit/loader-field-filters.test.ts index f5970c1d9c..427f85def0 100644 --- a/packages/core/tests/unit/loader-field-filters.test.ts +++ b/packages/core/tests/unit/loader-field-filters.test.ts @@ -2,6 +2,7 @@ import type { Kysely } from "kysely"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { handleContentCreate } from "../../src/api/index.js"; +import { TaxonomyRepository } from "../../src/database/repositories/taxonomy.js"; import type { Database } from "../../src/database/types.js"; import { emdashLoader } from "../../src/loader.js"; import { runWithContext } from "../../src/request-context.js"; @@ -264,14 +265,9 @@ describe("Loader field filters", () => { const postA = await createPost("Tech Post"); await createPost("Other Post"); - await db - .insertInto("content_taxonomies" as never) - .values({ - collection: "post", - entry_id: postA.id, - taxonomy_id: "tax_cat_tech", - } as never) - .execute(); + // Attach through the repository so the pivot row is stamped with the + // entry's denormalized filter/sort columns (migration 051). + await new TaxonomyRepository(db).attachToEntry("post", postA.id, "tax_cat_tech"); const loader = emdashLoader(); const result = await runWithContext({ editMode: false, db }, () => diff --git a/packages/core/tests/unit/loader-taxonomy-filter.test.ts b/packages/core/tests/unit/loader-taxonomy-filter.test.ts index a692aaa0e8..498a30c123 100644 --- a/packages/core/tests/unit/loader-taxonomy-filter.test.ts +++ b/packages/core/tests/unit/loader-taxonomy-filter.test.ts @@ -2,6 +2,7 @@ import type { Kysely } from "kysely"; import { it, expect, beforeEach, afterEach } from "vitest"; import { handleContentCreate } from "../../src/api/index.js"; +import { TaxonomyRepository } from "../../src/database/repositories/taxonomy.js"; import type { Database } from "../../src/database/types.js"; import { emdashLoader } from "../../src/loader.js"; import { runWithContext } from "../../src/request-context.js"; @@ -55,10 +56,11 @@ describeEachDialect("Loader taxonomy term filter", (dialectName: DialectName) => } async function tag(contentId: string, taxonomyId: string) { - await db - .insertInto("content_taxonomies" as never) - .values({ collection: "post", entry_id: contentId, taxonomy_id: taxonomyId } as never) - .execute(); + // Attach through the repository so the pivot row is stamped with the + // entry's denormalized filter/sort columns (migration 051) — the state the + // backfill and write-path re-stamp guarantee in production. The + // pivot-drive listing seeks on those columns. + await new TaxonomyRepository(db).attachToEntry("post", contentId, taxonomyId); } function load(where: Record, locale?: string) { diff --git a/packages/core/tests/unit/loader-taxonomy-pivot.test.ts b/packages/core/tests/unit/loader-taxonomy-pivot.test.ts new file mode 100644 index 0000000000..5847b56d97 --- /dev/null +++ b/packages/core/tests/unit/loader-taxonomy-pivot.test.ts @@ -0,0 +1,498 @@ +/** + * Pivot-driven taxonomy listing (#1834). + * + * A taxonomy-filtered listing seeks the matching entries on the denormalized + * `content_taxonomies` pivot (migration 051) instead of scanning the whole + * collection. Two invariants: + * + * 1. **Parity** — when the pivot agrees with `ec_*` (the steady state the + * backfill + write-path re-stamp guarantee), the seek returns the same rows + * the old EXISTS shape did. + * 2. **Correctness under non-atomic writes** — when the pivot disagrees with + * `ec_*` (the transient window on D1, which has no transactions), the joined + * `ec_*` row decides membership. A stale pivot can under-fill a page; it can + * never leak a deleted or wrong-status/locale row. + */ + +import type { Kysely } from "kysely"; +import { afterEach, beforeEach, expect, it } from "vitest"; + +import { ContentRepository } from "../../src/database/repositories/content.js"; +import { TaxonomyRepository } from "../../src/database/repositories/taxonomy.js"; +import type { Database } from "../../src/database/types.js"; +import { buildTaxonomyPivotQuery, emdashLoader } from "../../src/loader.js"; +import { runWithContext } from "../../src/request-context.js"; +import { + describeEachDialect, + setupForDialectWithCollections, + teardownForDialect, + type DialectName, + type DialectTestContext, +} from "../utils/test-db.js"; + +describeEachDialect("Loader taxonomy pivot-drive", (dialectName: DialectName) => { + let ctx: DialectTestContext; + let db: Kysely; + let content: ContentRepository; + let tax: TaxonomyRepository; + let termSeq = 0; + + beforeEach(async () => { + ctx = await setupForDialectWithCollections(dialectName); + db = ctx.db; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- schema vs Database type + content = new ContentRepository(db as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- schema vs Database type + tax = new TaxonomyRepository(db as any); + termSeq = 0; + }); + + afterEach(async () => { + await teardownForDialect(ctx); + }); + + /** Insert a taxonomy term (translation_group = id) and return its id. */ + async function term(name: string, slug: string) { + const id = `tax_${name}_${slug}_${termSeq++}`; + await db + .insertInto("taxonomies" as never) + .values({ id, name, slug, label: slug, translation_group: id } as never) + .execute(); + return id; + } + + async function createPost( + title: string, + opts: { + status?: string; + publishedAt?: string; + createdAt?: string; + locale?: string; + } = {}, + ) { + return content.create({ + type: "post", + slug: `${title.toLowerCase().replace(/\s+/g, "-")}-${termSeq}`, + data: { title }, + status: opts.status ?? "published", + publishedAt: opts.publishedAt, + createdAt: opts.createdAt, + locale: opts.locale, + }); + } + + function load(where: Record, extra: Record = {}) { + const loader = emdashLoader(); + return runWithContext({ editMode: false, db }, () => + loader.loadCollection!({ + filter: { type: "post", where: where as never, ...extra }, + }), + ); + } + + function titles(result: { entries: { data: Record }[] }) { + return result.entries.map((e) => e.data.title); + } + + /** Order-insensitive comparison helper. */ + const sortAsc = (values: unknown[]) => + values.toSorted((a, b) => String(a).localeCompare(String(b))); + + // --- Parity ------------------------------------------------------------- + + it("selective term: returns only the one tagged entry", async () => { + const news = await term("category", "news"); + const hit = await createPost("Hit"); + for (let i = 0; i < 20; i++) await createPost(`Filler ${i}`); + await tax.attachToEntry("post", hit.id, news); + + const result = await load({ category: "news" }); + expect(titles(result)).toEqual(["Hit"]); + }); + + it("broad term (~half the collection): returns every tagged entry", async () => { + const news = await term("category", "news"); + const tagged: string[] = []; + for (let i = 0; i < 10; i++) { + const post = await createPost(`Post ${i}`); + if (i % 2 === 0) { + await tax.attachToEntry("post", post.id, news); + tagged.push(`Post ${i}`); + } + } + + const result = await load({ category: "news" }); + expect(sortAsc(titles(result))).toEqual(sortAsc(tagged)); + }); + + it("OR within a taxonomy: dedups an entry tagged with two matched terms", async () => { + const news = await term("category", "news"); + const sports = await term("category", "sports"); + const both = await createPost("Both"); + await tax.attachToEntry("post", both.id, news); + await tax.attachToEntry("post", both.id, sports); + + const result = await load({ category: ["news", "sports"] }); + // One row despite matching two groups (GROUP BY dedup). + expect(titles(result)).toEqual(["Both"]); + }); + + it("multi-term AND: only entries tagged in every requested taxonomy match", async () => { + const news = await term("category", "news"); + const featured = await term("tag", "featured"); + const both = await createPost("Both"); + const newsOnly = await createPost("News Only"); + await tax.attachToEntry("post", both.id, news); + await tax.attachToEntry("post", both.id, featured); + await tax.attachToEntry("post", newsOnly.id, news); + + const result = await load({ category: ["news"], tag: ["featured"] }); + expect(titles(result)).toEqual(["Both"]); + }); + + it("sorts by published_at (indexed) descending", async () => { + const news = await term("category", "news"); + const a = await createPost("Old", { publishedAt: "2020-01-01T00:00:00Z" }); + const b = await createPost("New", { publishedAt: "2024-01-01T00:00:00Z" }); + const c = await createPost("Mid", { publishedAt: "2022-01-01T00:00:00Z" }); + for (const p of [a, b, c]) await tax.attachToEntry("post", p.id, news); + + const result = await load({ category: "news" }, { orderBy: { published_at: "desc" } }); + expect(titles(result)).toEqual(["New", "Mid", "Old"]); + }); + + it("sorts by created_at (indexed, loader default) descending", async () => { + const news = await term("category", "news"); + const a = await createPost("First", { createdAt: "2020-01-01T00:00:00Z" }); + const b = await createPost("Third", { createdAt: "2024-01-01T00:00:00Z" }); + const c = await createPost("Second", { createdAt: "2022-01-01T00:00:00Z" }); + for (const p of [a, b, c]) await tax.attachToEntry("post", p.id, news); + + const result = await load({ category: "news" }); + expect(titles(result)).toEqual(["Third", "Second", "First"]); + }); + + it("sorts by updated_at (temp-sort path, not denormalized) descending", async () => { + const news = await term("category", "news"); + const a = await createPost("A"); + const b = await createPost("B"); + for (const p of [a, b]) await tax.attachToEntry("post", p.id, news); + // updated_at is read from the joined ec_* row in the temp-sort path (it is + // not a pivot column), so set distinct values directly for a deterministic + // order rather than relying on same-millisecond `now` timestamps. + await db + .updateTable("ec_post" as never) + .set({ updated_at: "2020-01-01T00:00:00Z" } as never) + .where("id" as never, "=", a.id) + .execute(); + await db + .updateTable("ec_post" as never) + .set({ updated_at: "2024-01-01T00:00:00Z" } as never) + .where("id" as never, "=", b.id) + .execute(); + + const result = await load({ category: "news" }, { orderBy: { updated_at: "desc" } }); + expect(titles(result)).toEqual(["B", "A"]); + }); + + it("filters by locale", async () => { + // A single term group with EN + FR variants sharing a slug, so the slug + // resolves in either locale (the loader scopes slug resolution by locale). + const group = "tax_category_news_group"; + await db + .insertInto("taxonomies" as never) + .values({ + id: group, + name: "category", + slug: "news", + label: "News", + locale: "en", + translation_group: group, + } as never) + .execute(); + await db + .insertInto("taxonomies" as never) + .values({ + id: "tax_category_news_fr", + name: "category", + slug: "news", + label: "Actualités", + locale: "fr", + translation_group: group, + } as never) + .execute(); + + const en = await createPost("English", { locale: "en" }); + const fr = await createPost("French", { locale: "fr" }); + await tax.attachToEntry("post", en.id, group); + await tax.attachToEntry("post", fr.id, group); + + // The FR listing returns only the FR entry — the pivot's `locale` column + // narrows it, even though both entries share the term group. + expect(titles(await load({ category: "news" }, { locale: "fr" }))).toEqual(["French"]); + expect(titles(await load({ category: "news" }, { locale: "en" }))).toEqual(["English"]); + }); + + it("filters by status: a draft query returns drafts, published returns published", async () => { + const news = await term("category", "news"); + const pub = await createPost("Published", { status: "published" }); + const draft = await createPost("Draft", { status: "draft" }); + await tax.attachToEntry("post", pub.id, news); + await tax.attachToEntry("post", draft.id, news); + + expect(titles(await load({ category: "news" }, { status: "published" }))).toEqual([ + "Published", + ]); + expect(titles(await load({ category: "news" }, { status: "draft" }))).toEqual(["Draft"]); + }); + + // --- Stale-pivot correctness (non-atomic-write guard) ------------------- + + /** + * Desync the pivot from `ec_*` by writing `ec_*` directly (bypassing the + * repository re-stamp) — the transient window a reader can observe on D1. + */ + async function desyncEcRow(id: string, set: Record): Promise { + await db + .updateTable("ec_post" as never) + .set(set as never) + .where("id" as never, "=", id) + .execute(); + } + + it("drops a row the pivot admits but ec_* has soft-deleted", async () => { + const news = await term("category", "news"); + const post = await createPost("Ghost"); + await tax.attachToEntry("post", post.id, news); + // Pivot still says live+published; ec_* is soft-deleted. + await desyncEcRow(post.id, { deleted_at: "2024-01-01T00:00:00Z" }); + + const result = await load({ category: "news" }); + expect(titles(result)).toEqual([]); + }); + + it("drops a row the pivot says published but ec_* has as draft", async () => { + const news = await term("category", "news"); + const post = await createPost("SecretDraft"); + await tax.attachToEntry("post", post.id, news); + await desyncEcRow(post.id, { status: "draft" }); + + const result = await load({ category: "news" }, { status: "published" }); + expect(titles(result)).toEqual([]); + }); + + it("drops a row whose ec_* locale no longer matches the filter", async () => { + const news = await term("category", "news"); + const post = await createPost("MovedLocale", { locale: "en" }); + await tax.attachToEntry("post", post.id, news); + await desyncEcRow(post.id, { locale: "fr" }); + + const result = await load({ category: "news" }, { locale: "en" }); + expect(titles(result)).toEqual([]); + }); + + it("under-fills (never returns wrong rows) when a stale row consumes a LIMIT slot", async () => { + const news = await term("category", "news"); + const valid: string[] = []; + for (let i = 0; i < 3; i++) { + const post = await createPost(`Valid ${i}`, { + publishedAt: `2020-0${i + 1}-01T00:00:00Z`, + }); + await tax.attachToEntry("post", post.id, news); + valid.push(`Valid ${i}`); + } + const stale = await createPost("Stale", { publishedAt: "2020-09-01T00:00:00Z" }); + await tax.attachToEntry("post", stale.id, news); + // Pivot says published; ec_* is a draft → dropped on the outer re-check. + await desyncEcRow(stale.id, { status: "draft" }); + + const result = await load({ category: "news" }, { orderBy: { published_at: "desc" } }); + // The stale row is absent; every returned row is valid (may under-fill). + expect(titles(result)).not.toContain("Stale"); + expect(sortAsc(titles(result))).toEqual(sortAsc(valid)); + }); + + // --- Re-stamp sync ------------------------------------------------------ + + it("publish re-stamps the pivot so the entry appears in a published listing", async () => { + const news = await term("category", "news"); + const post = await createPost("Draft", { status: "draft" }); + await tax.attachToEntry("post", post.id, news); + + expect(titles(await load({ category: "news" }, { status: "published" }))).toEqual([]); + + await content.publish("post", post.id); + expect(titles(await load({ category: "news" }, { status: "published" }))).toEqual(["Draft"]); + }); + + it("soft-delete then restore re-stamps the pivot both ways", async () => { + const news = await term("category", "news"); + const post = await createPost("Toggle"); + await tax.attachToEntry("post", post.id, news); + expect(titles(await load({ category: "news" }))).toEqual(["Toggle"]); + + await content.delete("post", post.id); + expect(titles(await load({ category: "news" }))).toEqual([]); + + await content.restore("post", post.id); + expect(titles(await load({ category: "news" }))).toEqual(["Toggle"]); + }); + + it("schedule re-stamps status+scheduled_at onto the pivot", async () => { + const news = await term("category", "news"); + const post = await createPost("Later", { status: "draft" }); + await tax.attachToEntry("post", post.id, news); + + const future = new Date(Date.now() + 86_400_000).toISOString(); + await content.schedule("post", post.id, future); + + const pivot = await db + .selectFrom("content_taxonomies") + .select(["status", "scheduled_at"]) + .where("entry_id", "=", post.id) + .executeTakeFirstOrThrow(); + expect(pivot.status).toBe("scheduled"); + expect(pivot.scheduled_at).toBe(future); + }); + + // --- Pagination --------------------------------------------------------- + + it("paginates by keyset cursor over the pivot sort key without overlap", async () => { + const news = await term("category", "news"); + const created: string[] = []; + for (let i = 0; i < 5; i++) { + const post = await createPost(`P${i}`, { publishedAt: `2020-0${i + 1}-01T00:00:00Z` }); + await tax.attachToEntry("post", post.id, news); + created.push(`P${i}`); + } + const expectedOrder = created.toReversed(); // published_at desc + + const seen: string[] = []; + let cursor: string | undefined; + for (let page = 0; page < 3; page++) { + const result: Awaited> = await load( + { category: "news" }, + { orderBy: { published_at: "desc" }, limit: 2, ...(cursor ? { cursor } : {}) }, + ); + seen.push(...titles(result)); + cursor = (result as { nextCursor?: string }).nextCursor; + if (!cursor) break; + } + expect(seen).toEqual(expectedOrder); + expect(new Set(seen).size).toBe(seen.length); // no overlap + }); + + it("paginates the OR-within-taxonomy (GROUP BY + HAVING cursor) path", async () => { + // Two matched terms → the multi-group branch (GROUP BY ct.entry_id, cursor + // in HAVING over MAX(sortval)). Each entry is tagged with BOTH terms so it + // fans out to two pivot rows and must still page as one row per entry. + const news = await term("category", "news"); + const sports = await term("category", "sports"); + const created: string[] = []; + for (let i = 0; i < 5; i++) { + const post = await createPost(`Q${i}`, { publishedAt: `2021-0${i + 1}-01T00:00:00Z` }); + await tax.attachToEntry("post", post.id, news); + await tax.attachToEntry("post", post.id, sports); + created.push(`Q${i}`); + } + const expectedOrder = created.toReversed(); + + const seen: string[] = []; + let cursor: string | undefined; + for (let page = 0; page < 4; page++) { + const result: Awaited> = await load( + { category: ["news", "sports"] }, + { orderBy: { published_at: "desc" }, limit: 2, ...(cursor ? { cursor } : {}) }, + ); + seen.push(...titles(result)); + cursor = (result as { nextCursor?: string }).nextCursor; + if (!cursor) break; + } + expect(seen).toEqual(expectedOrder); + expect(new Set(seen).size).toBe(seen.length); // no dup fan-out, no overlap + }); + + // --- Byline filter rides along in the pivot CTE ------------------------- + + it("ANDs a byline filter with the taxonomy filter on the pivot path", async () => { + const news = await term("category", "news"); + const both = await createPost("Bob News"); + const taxOnly = await createPost("News Only"); + const bylineOnly = await createPost("Bob Only"); + await tax.attachToEntry("post", both.id, news); + await tax.attachToEntry("post", taxOnly.id, news); + await tax.attachToEntry("post", bylineOnly.id, news); // tagged but wrong byline + const credit = async (id: string, group: string) => + db + .insertInto("_emdash_content_bylines" as never) + .values({ + id: `cb_${id}`, + collection_slug: "post", + content_id: id, + byline_id: group, + sort_order: 0, + } as never) + .execute(); + await credit(both.id, "byline_bob"); + await credit(bylineOnly.id, "byline_alice"); + + const result = await load({ category: "news", byline: "byline_bob" }); + expect(titles(result)).toEqual(["Bob News"]); + }); + + // --- Admin-shape parity (builder targeted directly) --------------------- + + it("builder serves the trash shape (deleted_at IS NOT NULL)", async () => { + const news = await term("category", "news"); + const live = await createPost("Live"); + const trashed = await createPost("Trashed"); + await tax.attachToEntry("post", live.id, news); + await tax.attachToEntry("post", trashed.id, news); + await content.delete("post", trashed.id); // re-stamps pivot deleted_at + + const rows = await buildTaxonomyPivotQuery({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- schema vs Database type + db: db as any, + collection: "post", + tableName: "ec_post", + groupSets: [[news]], + orderBy: undefined, + cursor: undefined, + locale: undefined, + status: undefined, // all statuses + deletedIsNull: false, // trash + bylineGroups: null, + fetchLimit: undefined, + offset: undefined, + }).execute(db); + + expect(rows.rows.map((r) => (r.title as string) ?? r.slug)).toEqual(["Trashed"]); + }); + + it("builder serves an all-statuses live shape (status undefined)", async () => { + const news = await term("category", "news"); + const pub = await createPost("Pub", { status: "published" }); + const draft = await createPost("Draft", { status: "draft" }); + await tax.attachToEntry("post", pub.id, news); + await tax.attachToEntry("post", draft.id, news); + + const rows = await buildTaxonomyPivotQuery({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- schema vs Database type + db: db as any, + collection: "post", + tableName: "ec_post", + groupSets: [[news]], + orderBy: undefined, + cursor: undefined, + locale: undefined, + status: undefined, // all statuses + deletedIsNull: true, // live + bylineGroups: null, + fetchLimit: undefined, + offset: undefined, + }).execute(db); + + const seen = sortAsc(rows.rows.map((r) => r.status as string)); + expect(seen).toEqual(["draft", "published"]); + }); +}); From 3adf276bb041044797319cd2d67f018435649170 Mon Sep 17 00:00:00 2001 From: "emdashbot[bot]" Date: Sat, 11 Jul 2026 18:06:24 +0000 Subject: [PATCH 2/2] ci: update query-count snapshots --- scripts/query-counts.queries.d1.json | 20 ++++++++++++-------- scripts/query-counts.queries.sqlite.json | 20 ++++++++++++-------- scripts/query-counts.snapshot.d1.json | 8 ++++---- scripts/query-counts.snapshot.sqlite.json | 8 ++++---- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/scripts/query-counts.queries.d1.json b/scripts/query-counts.queries.d1.json index 8817345ae2..932c41f6f4 100644 --- a/scripts/query-counts.queries.d1.json +++ b/scripts/query-counts.queries.d1.json @@ -40,11 +40,12 @@ "select * from \"taxonomies\" where \"name\" = ? and \"slug\" = ? order by \"locale\" asc": 1, "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, - "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.locale = \"ec_posts\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_posts\".id AND b.locale = \"ec_posts\".locale) AS \"_emdash_bylines\" FROM \"ec_posts\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND EXISTS ( SELECT 1 FROM content_taxonomies ct INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.name = ? AND t.slug in (...) ) ORDER BY \"published_at\" DESC, \"id\" DESC": 1, "select count(*) as \"count\" from \"_emdash_collections\"": 1, "SELECT COUNT(*) as count FROM \"_emdash_migrations\"": 1, + "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, - "UPDATE _emdash_cron_tasks SET status = 'idle', locked_at = NULL WHERE status = 'running' AND locked_at < ?": 1 + "UPDATE _emdash_cron_tasks SET status = 'idle', locked_at = NULL WHERE status = 'running' AND locked_at < ?": 1, + "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /category/development (warm)": { "select \"a\".\"id\" as \"a_id\", \"a\".\"name\" as \"a_name\", \"a\".\"label\" as \"a_label\", \"a\".\"description\" as \"a_description\", \"w\".\"id\" as \"w_id\", \"w\".\"type\" as \"w_type\", \"w\".\"title\" as \"w_title\", \"w\".\"content\" as \"w_content\", \"w\".\"menu_name\" as \"w_menu_name\", \"w\".\"component_id\" as \"w_component_id\", \"w\".\"component_props\" as \"w_component_props\", \"w\".\"area_id\" as \"w_area_id\", \"w\".\"sort_order\" as \"w_sort_order\", \"w\".\"created_at\" as \"w_created_at\" from \"_emdash_widget_areas\" as \"a\" left join \"_emdash_widgets\" as \"w\" on \"w\".\"area_id\" = \"a\".\"id\" where \"a\".\"name\" = ? order by \"w\".\"sort_order\" asc": 1, @@ -55,8 +56,9 @@ "select * from \"taxonomies\" where \"name\" = ? and \"slug\" = ? order by \"locale\" asc": 1, "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, - "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.locale = \"ec_posts\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_posts\".id AND b.locale = \"ec_posts\".locale) AS \"_emdash_bylines\" FROM \"ec_posts\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND EXISTS ( SELECT 1 FROM content_taxonomies ct INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.name = ? AND t.slug in (...) ) ORDER BY \"published_at\" DESC, \"id\" DESC": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1 + "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /contributors (cold)": { "select \"a\".\"id\" as \"a_id\", \"a\".\"name\" as \"a_name\", \"a\".\"label\" as \"a_label\", \"a\".\"description\" as \"a_description\", \"w\".\"id\" as \"w_id\", \"w\".\"type\" as \"w_type\", \"w\".\"title\" as \"w_title\", \"w\".\"content\" as \"w_content\", \"w\".\"menu_name\" as \"w_menu_name\", \"w\".\"component_id\" as \"w_component_id\", \"w\".\"component_props\" as \"w_component_props\", \"w\".\"area_id\" as \"w_area_id\", \"w\".\"sort_order\" as \"w_sort_order\", \"w\".\"created_at\" as \"w_created_at\" from \"_emdash_widget_areas\" as \"a\" left join \"_emdash_widgets\" as \"w\" on \"w\".\"area_id\" = \"a\".\"id\" where \"a\".\"name\" = ? order by \"w\".\"sort_order\" asc": 1, @@ -268,11 +270,12 @@ "select * from \"taxonomies\" where \"name\" = ? and \"slug\" = ? order by \"locale\" asc": 1, "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, - "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.locale = \"ec_posts\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_posts\".id AND b.locale = \"ec_posts\".locale) AS \"_emdash_bylines\" FROM \"ec_posts\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND EXISTS ( SELECT 1 FROM content_taxonomies ct INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.name = ? AND t.slug in (...) ) ORDER BY \"published_at\" DESC, \"id\" DESC": 1, "select count(*) as \"count\" from \"_emdash_collections\"": 1, "SELECT COUNT(*) as count FROM \"_emdash_migrations\"": 1, + "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, - "UPDATE _emdash_cron_tasks SET status = 'idle', locked_at = NULL WHERE status = 'running' AND locked_at < ?": 1 + "UPDATE _emdash_cron_tasks SET status = 'idle', locked_at = NULL WHERE status = 'running' AND locked_at < ?": 1, + "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /tag/webdev (warm)": { "select \"a\".\"id\" as \"a_id\", \"a\".\"name\" as \"a_name\", \"a\".\"label\" as \"a_label\", \"a\".\"description\" as \"a_description\", \"w\".\"id\" as \"w_id\", \"w\".\"type\" as \"w_type\", \"w\".\"title\" as \"w_title\", \"w\".\"content\" as \"w_content\", \"w\".\"menu_name\" as \"w_menu_name\", \"w\".\"component_id\" as \"w_component_id\", \"w\".\"component_props\" as \"w_component_props\", \"w\".\"area_id\" as \"w_area_id\", \"w\".\"sort_order\" as \"w_sort_order\", \"w\".\"created_at\" as \"w_created_at\" from \"_emdash_widget_areas\" as \"a\" left join \"_emdash_widgets\" as \"w\" on \"w\".\"area_id\" = \"a\".\"id\" where \"a\".\"name\" = ? order by \"w\".\"sort_order\" asc": 1, @@ -283,7 +286,8 @@ "select * from \"taxonomies\" where \"name\" = ? and \"slug\" = ? order by \"locale\" asc": 1, "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, - "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.locale = \"ec_posts\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_posts\".id AND b.locale = \"ec_posts\".locale) AS \"_emdash_bylines\" FROM \"ec_posts\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND EXISTS ( SELECT 1 FROM content_taxonomies ct INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.name = ? AND t.slug in (...) ) ORDER BY \"published_at\" DESC, \"id\" DESC": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1 + "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 } } diff --git a/scripts/query-counts.queries.sqlite.json b/scripts/query-counts.queries.sqlite.json index 293d6a8839..23f5ff3ee9 100644 --- a/scripts/query-counts.queries.sqlite.json +++ b/scripts/query-counts.queries.sqlite.json @@ -25,8 +25,9 @@ "select * from \"taxonomies\" where \"name\" = ? and \"slug\" = ? order by \"locale\" asc": 1, "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, - "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.locale = \"ec_posts\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_posts\".id AND b.locale = \"ec_posts\".locale) AS \"_emdash_bylines\" FROM \"ec_posts\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND EXISTS ( SELECT 1 FROM content_taxonomies ct INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.name = ? AND t.slug in (...) ) ORDER BY \"published_at\" DESC, \"id\" DESC": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1 + "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /category/development (warm)": { "select \"a\".\"id\" as \"a_id\", \"a\".\"name\" as \"a_name\", \"a\".\"label\" as \"a_label\", \"a\".\"description\" as \"a_description\", \"w\".\"id\" as \"w_id\", \"w\".\"type\" as \"w_type\", \"w\".\"title\" as \"w_title\", \"w\".\"content\" as \"w_content\", \"w\".\"menu_name\" as \"w_menu_name\", \"w\".\"component_id\" as \"w_component_id\", \"w\".\"component_props\" as \"w_component_props\", \"w\".\"area_id\" as \"w_area_id\", \"w\".\"sort_order\" as \"w_sort_order\", \"w\".\"created_at\" as \"w_created_at\" from \"_emdash_widget_areas\" as \"a\" left join \"_emdash_widgets\" as \"w\" on \"w\".\"area_id\" = \"a\".\"id\" where \"a\".\"name\" = ? order by \"w\".\"sort_order\" asc": 1, @@ -37,8 +38,9 @@ "select * from \"taxonomies\" where \"name\" = ? and \"slug\" = ? order by \"locale\" asc": 1, "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, - "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.locale = \"ec_posts\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_posts\".id AND b.locale = \"ec_posts\".locale) AS \"_emdash_bylines\" FROM \"ec_posts\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND EXISTS ( SELECT 1 FROM content_taxonomies ct INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.name = ? AND t.slug in (...) ) ORDER BY \"published_at\" DESC, \"id\" DESC": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1 + "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /contributors (cold)": { "select \"a\".\"id\" as \"a_id\", \"a\".\"name\" as \"a_name\", \"a\".\"label\" as \"a_label\", \"a\".\"description\" as \"a_description\", \"w\".\"id\" as \"w_id\", \"w\".\"type\" as \"w_type\", \"w\".\"title\" as \"w_title\", \"w\".\"content\" as \"w_content\", \"w\".\"menu_name\" as \"w_menu_name\", \"w\".\"component_id\" as \"w_component_id\", \"w\".\"component_props\" as \"w_component_props\", \"w\".\"area_id\" as \"w_area_id\", \"w\".\"sort_order\" as \"w_sort_order\", \"w\".\"created_at\" as \"w_created_at\" from \"_emdash_widget_areas\" as \"a\" left join \"_emdash_widgets\" as \"w\" on \"w\".\"area_id\" = \"a\".\"id\" where \"a\".\"name\" = ? order by \"w\".\"sort_order\" asc": 1, @@ -181,8 +183,9 @@ "select * from \"taxonomies\" where \"name\" = ? and \"slug\" = ? order by \"locale\" asc": 1, "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, - "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.locale = \"ec_posts\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_posts\".id AND b.locale = \"ec_posts\".locale) AS \"_emdash_bylines\" FROM \"ec_posts\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND EXISTS ( SELECT 1 FROM content_taxonomies ct INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.name = ? AND t.slug in (...) ) ORDER BY \"published_at\" DESC, \"id\" DESC": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1 + "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /tag/webdev (warm)": { "select \"a\".\"id\" as \"a_id\", \"a\".\"name\" as \"a_name\", \"a\".\"label\" as \"a_label\", \"a\".\"description\" as \"a_description\", \"w\".\"id\" as \"w_id\", \"w\".\"type\" as \"w_type\", \"w\".\"title\" as \"w_title\", \"w\".\"content\" as \"w_content\", \"w\".\"menu_name\" as \"w_menu_name\", \"w\".\"component_id\" as \"w_component_id\", \"w\".\"component_props\" as \"w_component_props\", \"w\".\"area_id\" as \"w_area_id\", \"w\".\"sort_order\" as \"w_sort_order\", \"w\".\"created_at\" as \"w_created_at\" from \"_emdash_widget_areas\" as \"a\" left join \"_emdash_widgets\" as \"w\" on \"w\".\"area_id\" = \"a\".\"id\" where \"a\".\"name\" = ? order by \"w\".\"sort_order\" asc": 1, @@ -193,7 +196,8 @@ "select * from \"taxonomies\" where \"name\" = ? and \"slug\" = ? order by \"locale\" asc": 1, "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, - "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.locale = \"ec_posts\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_posts\".id AND b.locale = \"ec_posts\".locale) AS \"_emdash_bylines\" FROM \"ec_posts\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND EXISTS ( SELECT 1 FROM content_taxonomies ct INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_posts\".id AND t.name = ? AND t.slug in (...) ) ORDER BY \"published_at\" DESC, \"id\" DESC": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1 + "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct INNER JOIN \"ec_posts\" AS e ON e.id = ct.entry_id WHERE ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 } } diff --git a/scripts/query-counts.snapshot.d1.json b/scripts/query-counts.snapshot.d1.json index b93ebe8f6d..5e316de883 100644 --- a/scripts/query-counts.snapshot.d1.json +++ b/scripts/query-counts.snapshot.d1.json @@ -1,8 +1,8 @@ { "GET / (cold)": 17, "GET / (warm)": 6, - "GET /category/development (cold)": 22, - "GET /category/development (warm)": 10, + "GET /category/development (cold)": 23, + "GET /category/development (warm)": 11, "GET /contributors (cold)": 17, "GET /contributors (warm)": 6, "GET /contributors-naive (cold)": 24, @@ -17,6 +17,6 @@ "GET /rss.xml (warm)": 2, "GET /search (cold)": 22, "GET /search (warm)": 11, - "GET /tag/webdev (cold)": 22, - "GET /tag/webdev (warm)": 10 + "GET /tag/webdev (cold)": 23, + "GET /tag/webdev (warm)": 11 } diff --git a/scripts/query-counts.snapshot.sqlite.json b/scripts/query-counts.snapshot.sqlite.json index 222f7d5ddf..1459d4c436 100644 --- a/scripts/query-counts.snapshot.sqlite.json +++ b/scripts/query-counts.snapshot.sqlite.json @@ -1,8 +1,8 @@ { "GET / (cold)": 6, "GET / (warm)": 6, - "GET /category/development (cold)": 11, - "GET /category/development (warm)": 10, + "GET /category/development (cold)": 12, + "GET /category/development (warm)": 11, "GET /contributors (cold)": 6, "GET /contributors (warm)": 6, "GET /contributors-naive (cold)": 13, @@ -17,6 +17,6 @@ "GET /rss.xml (warm)": 2, "GET /search (cold)": 11, "GET /search (warm)": 11, - "GET /tag/webdev (cold)": 10, - "GET /tag/webdev (warm)": 10 + "GET /tag/webdev (cold)": 11, + "GET /tag/webdev (warm)": 11 }