Skip to content

feat(drizzle): support json sub-path where in polymorphic joins - #18236

Draft
r1tsuu wants to merge 2 commits into
feat/polymorphic-join-where-restore-support-3xfrom
polymorphic-join-where-json-subpath-3x
Draft

r1tsuu wants to merge 2 commits into
feat/polymorphic-join-where-restore-support-3xfrom
polymorphic-join-where-json-subpath-3x

Conversation

@r1tsuu

@r1tsuu r1tsuu commented Sep 21, 2026

Copy link
Copy Markdown
Member

Written by AI

Draft. Stacked on #18235 — follow-up to 9339d63 (PYLD-3840). Restores another of the polymorphic-join where shapes that 9339d63 made fail-closed. Base is #18235 branch so the diff shows only this change; retarget to 3.x once #18235 lands or is dropped.

What this restores

Constraints that reach into a json field, e.g. { 'settings.approved': { equals: true } }. Of the categories 9339d63 disabled this is the cheapest: it needs no per-branch join mechanism, only the JSON traversal each adapter already uses on the non-polymorphic query path (createJSONQuery on postgres, convertPathToJSONTraversal on sqlite).

Each branch compiles its own traversal against its own json column, so the combined caller + access where still applies in full to every branch — nothing dropped or truncated (no PYLD-3840 regression).

A target collection that has no json column at the path reads as SQL NULL and resolves to a boolean constant derived from the operator alone, rather than borrowing another branch's field to sanitize the value. That borrow is the cross-cutting fail-open hazard called out in the plan tracked on the main PR #18233, and this is the first category to avoid it.

Shapes that cannot be compiled soundly stay fail-closed:

  • negated operators (not_equals, not_in, not_like) — on an absent branch "key missing" and "value does not match" are indistinguishable, so a negated constraint could widen the access rule it came from
  • null and non-scalar values, and mixed-type in lists
  • localized json fields
  • a sub-path that is a real column or a has-many value table in another target
  • adapters with no JSON traversal

Still deferred

Localized fields, array/blocks rows, relationship/upload traversal and the geo operators remain fail-closed. The per-category plan — including the text-vs-number fail-open hazard (Number('x')→NaN→null→IS NULL matching all value-less rows) that must be guarded before category 5 can ever be enabled — is tracked in #370.

One correction to that plan worth recording: the equals + sanitized-null → IS NULL conversion is not polymorphic-specific. parseParams.ts:379 does the same thing, so {numberField: {equals: 'x'}} already matches all value-less rows on the ordinary query path. It remains the right blocker for mixing text and number branches (one shared value coercing to null on just the number branch is new), but the guard belongs upstream in parseParams, not here.

Verification

  • Unit (packages/drizzle/src/find): 106 pass (was 85)
  • test/joins/int.spec.ts: 118 pass / 14 skip (sqlite), 120 pass / 12 skip (postgres) — exactly +4 tests over the base branch, no pass→skip drift
  • tsc, eslint and prettier clean

Independently written and run against #18235 branch. Left as a draft for review — not for merge.

Companion: #18235 (base), and the follow-up that restores localized and separate-row paths on top of this one.

Follow-up fix (second commit)

Review of this branch turned up a bug that made the feature only half-work. getFieldByPath does not narrow its field list when a segment's field has no subfields, so title.tags resolves to an unrelated top-level tags field. Two consequences in the plan builder:

  • a json sub-path whose key matched another field name never reached the JSON handler, so only keys with no namesake worked — settings.approved compiled, settings.tags did not
  • the bogus plan then looked for a has-many select value table that cannot exist and threw a raw Error, i.e. a 500 rather than a 400 QueryError, reachable from an access rule

The plan builder now confirms every ancestor segment is a container it can descend into before trusting the resolved field. Both added integration tests fail without the change — title.tags with the raw "has no value table" error, and settings.title by not matching.

Updated verification: unit 108 pass, test/joins/int.spec.ts 120 pass / 14 skip (sqlite) and 122 pass / 12 skip (postgres).

Known issue inherited from the base

Not fixed here: the base branch has a separate fail-open on contains over a mixedSelect path. contains is rewritten to an exact match only when field.hasMany, so the single-select branch compiles to a substring ilike and admits any option value containing the permitted one as a substring on SQLite, while failing outright against a Postgres enum column. A tested patch for that exists but belongs on the base PR.

Restores `json` sub-path constraints (e.g. `settings.approved`) in polymorphic
join `where` clauses, which #290 made fail closed. This is the cheapest of the
categories that PR disabled: it needs no per-branch join mechanism, only the
JSON traversal each adapter already uses on the non-polymorphic query path.

Each branch compiles its own traversal against its own `json` column, so the
combined caller + access `where` still applies in full to every branch. A target
collection without the `json` column reads as SQL NULL and resolves to a boolean
constant derived from the operator alone, rather than borrowing another branch's
field to sanitize the value.

Shapes that cannot be compiled soundly stay fail closed: negated operators
(`not_equals`, `not_in`, `not_like`), null and non-scalar values, mixed-type `in`
lists, localized `json` fields, a sub-path that is a real column or has-many
value table in another target, and adapters with no JSON traversal.
…name

`getFieldByPath` does not narrow its field list when a segment's field has no
subfields, so `title.tags` resolves to an unrelated top-level `tags` field. In
the polymorphic plan builder that produced two wrong outcomes:

- a `json` sub-path whose key matches another field name never reached the JSON
  handler at all, so only keys with no namesake worked
- the bogus plan then looked for a has-many select value table that cannot
  exist and threw a raw `Error`, i.e. a 500 rather than a 400 `QueryError`

The plan builder now confirms every ancestor segment is a container it can
descend into before trusting the resolved field. A path that fails that check
falls through to the `json` sub-path and unsupported-shape checks, so
`settings.title` compiles as a JSON traversal and `title.tags` fails closed
with a `QueryError`.

Both new integration tests fail without this change — `title.tags` with the
raw "has no value table" error, and `settings.title` by not matching.
@github-actions

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 1.00 MB 🆕 Added
packages/payload/meta_index.json esbuild/index.js 1.47 MB 🆕 Added
packages/payload/meta_shared.json esbuild/exports/shared.js 202.55 KB 🆕 Added
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 290.90 KB 🆕 Added
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 1.19 MB 🆕 Added
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 16.32 KB 🆕 Added
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████▌ }}}$ 82.4%, 820.17 KB
dist/views/Version ${{\color{Goldenrod}{ █▎ }}}$ 5.2%, 51.86 KB
dist/views/Dashboard ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 21.39 KB
dist/views/Document ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 16.71 KB
dist/views/List ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 11.42 KB
dist/views/Root ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 9.90 KB
dist/views/Versions ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 6.17 KB
dist/views/API ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 6.13 KB
dist/elements/Nav ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 5.96 KB
dist/views/Account ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 5.56 KB
dist/elements/DocumentHeader ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 4.84 KB
dist/views/ForgotPassword ${{\color{Goldenrod}{ }}}$ 0.4%, 4.45 KB
dist/views/Login ${{\color{Goldenrod}{ }}}$ 0.4%, 4.40 KB
dist/layouts/Root ${{\color{Goldenrod}{ }}}$ 0.3%, 3.19 KB
dist/views/CreateFirstUser ${{\color{Goldenrod}{ }}}$ 0.3%, 2.81 KB
dist/templates/Default ${{\color{Goldenrod}{ }}}$ 0.3%, 2.64 KB
dist/views/BrowseByFolder ${{\color{Goldenrod}{ }}}$ 0.3%, 2.61 KB
dist/views/CollectionFolders ${{\color{Goldenrod}{ }}}$ 0.2%, 2.44 KB
dist/views/ResetPassword ${{\color{Goldenrod}{ }}}$ 0.2%, 2.40 KB
dist/views/Logout ${{\color{Goldenrod}{ }}}$ 0.2%, 1.94 KB
(other) ${{\color{Goldenrod}{ ████▍ }}}$ 17.6%, 175.39 KB

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████▋ }}}$ 66.8%, 974.49 KB
dist/collections/operations ${{\color{Goldenrod}{ ▊ }}}$ 3.4%, 49.17 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 46.02 KB
dist/versions/migrations ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 18.50 KB
dist/auth/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 16.98 KB
dist/globals/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 14.75 KB
dist/fields/config ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 14.17 KB
dist/queues/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 13.15 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 13.13 KB
dist/utilities/telemetry ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 11.40 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 10.62 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 9.29 KB
dist/bin/generateImportMap ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 9.08 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.73 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.35 KB
dist/index.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.57 KB
dist/database/migrations ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.54 KB
dist/auth/strategies ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 6.97 KB
dist/auth/endpoints ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 6.59 KB
dist/config/sanitize.js ${{\color{Goldenrod}{ }}}$ 0.4%, 6.30 KB
(other) ${{\color{Goldenrod}{ ████████▎ }}}$ 33.2%, 484.32 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ███████████████████▊ }}}$ 79.2%, 157.26 KB
dist/fields/validations.js ${{\color{Goldenrod}{ █▎ }}}$ 5.3%, 10.60 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 3.35 KB
dist/config/orderable ${{\color{Goldenrod}{ ▍ }}}$ 1.6%, 3.13 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 2.79 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 1.42 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 1.28 KB
dist/utilities/getVersionsConfig.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 1.04 KB
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 943 B
dist/folders/utils ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 916 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ }}}$ 0.4%, 850 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ }}}$ 0.4%, 713 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 651 B
dist/utilities/fieldPath.js ${{\color{Goldenrod}{ }}}$ 0.3%, 639 B
dist/utilities/getSafeRedirect.js ${{\color{Goldenrod}{ }}}$ 0.3%, 632 B
dist/uploads/getFileTypeIdentity.js ${{\color{Goldenrod}{ }}}$ 0.3%, 625 B
dist/collections/config ${{\color{Goldenrod}{ }}}$ 0.3%, 570 B
dist/bin/generateImportMap ${{\color{Goldenrod}{ }}}$ 0.3%, 561 B
dist/auth/sessions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 525 B
(other) ${{\color{Goldenrod}{ █████▏ }}}$ 20.8%, 41.40 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/features/blocks ${{\color{Goldenrod}{ ███▏ }}}$ 12.7%, 36.44 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▊ }}}$ 11.4%, 32.65 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 24.36 KB
dist/features/experimental_table ${{\color{Goldenrod}{ ██ }}}$ 8.2%, 23.66 KB
dist/packages/@lexical ${{\color{Goldenrod}{ █▋ }}}$ 6.9%, 19.80 KB
dist/features/link ${{\color{Goldenrod}{ █▋ }}}$ 6.5%, 18.63 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▍ }}}$ 5.6%, 16.08 KB
dist/features/upload ${{\color{Goldenrod}{ █▏ }}}$ 4.8%, 13.92 KB
dist/features/textState ${{\color{Goldenrod}{ ▉ }}}$ 3.9%, 11.08 KB
dist/features/relationship ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 9.10 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▊ }}}$ 3.1%, 8.79 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 8.40 KB
dist/features/debug ${{\color{Goldenrod}{ ▋ }}}$ 2.6%, 7.40 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 7.18 KB
dist/lexical/config ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 5.08 KB
dist/features/lists ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 5.03 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 3.46 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.23 KB
dist/field/Field.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 2.81 KB
dist/lexical/nodes ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.66 KB
(other) ${{\color{Goldenrod}{ █████████████████████▊ }}}$ 87.3%, 251.16 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████▎ }}}$ 49.1%, 579.15 KB
dist/elements/FolderView ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 29.40 KB
dist/elements/BulkUpload ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 28.57 KB
dist/views/Edit ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 21.17 KB
dist/elements/WhereBuilder ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 17.36 KB
dist/forms/Form ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 16.05 KB
dist/elements/Table ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 15.80 KB
dist/fields/Relationship ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 15.79 KB
dist/fields/Upload ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 14.24 KB
dist/fields/Blocks ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 13.90 KB
dist/elements/QueryPresets ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 10.36 KB
dist/elements/PublishButton ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.59 KB
dist/providers/Folders ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.46 KB
dist/elements/HTMLDiff ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.38 KB
dist/views/CollectionFolder ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.83 KB
dist/fields/Array ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.73 KB
dist/views/List ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.36 KB
dist/elements/ReactSelect ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.33 KB
dist/elements/ListHeader ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.12 KB
dist/elements/LivePreview ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.03 KB
(other) ${{\color{Goldenrod}{ ████████████▋ }}}$ 50.9%, 601.04 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ █████ }}}$ 20.0%, 3.12 KB
../../node_modules ${{\color{Goldenrod}{ ████▎ }}}$ 17.0%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██▍ }}}$ 9.8%, 1.52 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▍ }}}$ 5.5%, 862 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █▎ }}}$ 5.2%, 814 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █▏ }}}$ 4.9%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █▏ }}}$ 4.8%, 756 B
dist/elements/Translation ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 440 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▋ }}}$ 2.6%, 399 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▌ }}}$ 2.2%, 339 B
dist/utilities/getVisibleEntities.js ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 329 B
dist/utilities/getNavGroups.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 301 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 232 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 180 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 159 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 152 B
dist/forms/Form ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 147 B
dist/utilities/abortAndIgnore.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 146 B
(other) ${{\color{Goldenrod}{ ████████████████████ }}}$ 80.0%, 12.51 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

This branch has not been deployed

No deployments
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.

1 participant