Skip to content

feat(content): support indexed custom field sorting - #2212

Merged
ascorbic merged 20 commits into
emdash-cms:mainfrom
logelog:feat/indexed-custom-field-sorting
Aug 13, 2026
Merged

feat(content): support indexed custom field sorting#2212
ascorbic merged 20 commits into
emdash-cms:mainfrom
logelog:feat/indexed-custom-field-sorting

Conversation

@logelog

@logelog logelog commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds server-backed sorting for custom scalar fields that are explicitly marked indexed.

Collection fields gain an opt-in indexed flag. EmDash validates that flag against supported scalar field types, maintains a normalized index table when content changes, and allows the existing orderBy contract to sort by those field slugs. Sorting remains database-backed and cursor-safe, so it applies to the complete result set instead of only the currently loaded admin page.

This enables uses such as SEO score, ticket priority, workflow rank, or other numeric and textual metadata without introducing full-table scans.

The collection administration contract this builds on has landed in emdash-cms/emdash PR 2194.

Addresses the structured sorting portion of emdash-cms/emdash issue 2179.
Discussion: indexed sorting; configured content-list fields.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: indexed sorting is awaiting maintainer review.

AI-generated code disclosure

  • This PR includes AI generated code: Claude Opus 5, GPT-5.6 (via private dev orchestra)

Screenshots / test output

Validated on the current main base at the time of the update:

  • focused SQLite: 282 tests passed
  • PostgreSQL 17.10 indexed ordering: 3 tests passed
  • focused admin: 71 tests passed
  • full workspace typecheck passed
  • type-aware lint passed
  • format check passed for 2,659 files

GitHub CI runs the complete branch matrix.

@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e1bf1d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Major
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 920 lines across 35 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@pkg-pr-new

pkg-pr-new Bot commented Jul 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2212

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2212

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2212

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2212

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2212

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2212

emdash

npm i https://pkg.pr.new/emdash@2212

create-emdash

npm i https://pkg.pr.new/create-emdash@2212

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2212

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2212

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2212

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2212

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2212

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2212

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2212

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2212

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2212

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2212

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2212

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2212

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2212

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2212

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2212

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2212

commit: 1e1bf1d

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server-side sorting approach is sound: it adds physical SQLite indexes on real ec_* columns for scalar field types, keeps pagination cursor-stable with explicit null ordering, and validates the indexed flag against a whitelist of indexable types. That fits EmDash’s schema-in-the-database architecture.

Two things block a clean sign-off:

  1. AGENTS.md Discussion requirement. The PR introduces a new user-facing feature (indexed custom-field sorting) but the description explicitly states that the linked Discussion #1717 only covers admin.listColumns (#2194) and “no Discussion has been opened for indexed sorting yet.” EmDash requires a maintainer-approved Discussion before merging a feature. This needs to be opened/approved before merge.

  2. FieldEditor bug when changing an indexed field to a non-indexable type. The editor sends indexed: undefined when the selected type is not indexable. The backend interprets undefined as “preserve existing value,” so an already-indexed field that is changed to a non-indexable type (e.g. stringtext) fails with FIELD_NOT_INDEXABLE instead of dropping the index. Fix by explicitly sending false for non-indexable types.

Other notes:

  • The diff still includes the stacked #2194 admin.listColumns commits/changeset. Review comments here focus on the indexed-sorting additions; the list-columns pieces should land via #2194.
  • Server support for custom-field sorting is complete, but the admin content list does not yet expose sort controls on custom columns (the new columns are display-only). If admin UI for selecting a custom sort is intended for this PR, it is missing.
  • I did not run tests/lint/typecheck (no shell/tooling). Test coverage looks reasonable for null ordering, equal-value stability, unindexed-field rejection, and index lifecycle; the query-plan assertions are brittle but directly verify the stated performance goal.

required,
unique,
searchable: isSearchableType ? searchable : undefined,
indexed: isIndexableType ? indexed : undefined,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] When the selected type is not indexable, this sends indexed: undefined. In updateField the backend computes nextIndexed = input.indexed ?? field.indexed, so an already-indexed field that is changed to a non-indexable type (e.g. stringtext, same column type) keeps indexed = true and assertIndexableField throws FIELD_NOT_INDEXABLE. The UI gives the user no way to recover because the Indexed switch has disappeared.

Send false explicitly for non-indexable types so the index is dropped and the type change can succeed:

Suggested change
indexed: isIndexableType ? indexed : undefined,
indexed: isIndexableType ? indexed : false,

"@emdash-cms/admin": minor
---

Adds opt-in database indexes for scalar custom fields and stable cursor pagination when ordering content lists by those fields.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] This changeset describes a new user-facing feature, but AGENTS.md requires a prior maintainer-approved Discussion for features. The PR description acknowledges that Discussion #1717 covers admin.listColumns (#2194) only and that no Discussion has been opened for indexed custom-field sorting yet. Please open and get approval for a Discussion covering this feature before merging.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 3, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Aug 3, 2026
@logelog
logelog force-pushed the feat/indexed-custom-field-sorting branch from 0688023 to 3cdeddc Compare August 12, 2026 17:35
const indexName = this.getFieldIndexName(fieldId);

await sql`
CREATE INDEX IF NOT EXISTS ${sql.ref(indexName)}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localised list queries filter by locale, but this index orders every locale together. On a collection where the requested locale contains only a small fraction of entries, the database must walk and discard rows from other locales to fill each page, approaching a collection-wide index scan. Please make locale-scoped custom-field ordering seekable,e.g. put locale before the ordering tuple

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Aug 13, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Aug 13, 2026

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ascorbic
ascorbic merged commit fefb702 into emdash-cms:main Aug 13, 2026
46 checks passed
@emdashbot emdashbot Bot mentioned this pull request Aug 13, 2026
MA2153 added a commit to MA2153/emdash that referenced this pull request Aug 13, 2026
Reconciles upstream's indexed custom field sorting (emdash-cms#2212) with this
branch's storage-less reference fields.

Conflict resolutions:

- registry.ts deleteField: drop the field index when `field.indexed`,
  then drop the column behind the `columnExists` guard. A field being
  storage-less is a property of the row, not the type, so pre-existing
  reference columns still need the DDL.
- seed/apply.ts existing-field path: keep `upsertSeedField`, which
  supersedes the inline update/create pair.
- seed/apply.ts new-collection path: keep the reference-relation loop
  and carry `indexed` through to the created field.
- upsertSeedField: pass `indexed` to both updateField and createField.
  Seeding an existing collection routes through here, so omitting it
  would drop the flag for every seeded field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants