Skip to content

feat(paddle_fdw): rich query pushdown, more entities, richer import schema#616

Merged
burmecia merged 3 commits into
supabase:mainfrom
heymcgovern:paddle-fdw-pushdown-entities
Jul 24, 2026
Merged

feat(paddle_fdw): rich query pushdown, more entities, richer import schema#616
burmecia merged 3 commits into
supabase:mainfrom
heymcgovern:paddle-fdw-pushdown-entities

Conversation

@heymcgovern

Copy link
Copy Markdown
Contributor

Hey team 👋 I'm a dev advocate at Paddle and a big fan of Supabase. Love that there's a Paddle wrapper 💛 - cheers for building this! Was digging into it and spotted a few places where we could improve things.

Once this lands, I'll get out some docs on our side + a developer changelog entry, and I'll include in our next roundup email newsletter (similar to your monthly "Supa Update" one).

What kind of change does this PR introduce?

Feature + docs, with a small bug fix.

Everything is contained in the Paddle Wasm FDW (wasm-wrappers/fdw/paddle_fdw), version bumped to 0.3.0.

What is the current behavior?

The Paddle wrapper only pushes down a single id equality. Any other WHERE clause falls back to a full paginated scan of the object. import foreign schema creates just three tables (customers, products, subscriptions) with minimal cols, so most fields have to be pulled out of attrs.

Two specific gaps:

  • subscriptions is missing from the id-pushdown allowlist, so where id = 'sub_...' does a full scan instead of a single-object GET
  • adjustments (refunds, credits, chargebacks) is half-wired. It has page-size tuning but no pushdown, import, ordocs).

What is the new behavior?

  • subscriptions id lookups now do a single-object GET.
  • Per-object filter pushdown on the list endpoints (e.g. customer_id, subscription_id, status, product_id — full list per entity in the docs), IN (...) pushed as Paddle comma-separated lists, and date-range pushdown for transactions (created_at / updated_at / billed_at). Filter values and the id path segment are percent-encoded.
  • adjustments is fully wired - this is esp. useful for chargebacks reporting.
  • import foreign schema now generates 7 typed tables and honors the limit to / except clauses.
  • A column that's absent from a given API response now maps to NULL instead of failing the whole scan.
  • docs/catalog/paddle.md updated to match. I also made a small tweak to our intro - we don't generally lead with being payments infrastructure as we're not a PSP.

Auth and the pinned Paddle-Version header are unchanged.

Additional context

Testing

  • Mock integration tests (cargo pgrx test --features "wasm_fdw pg15") cover the single-object id GET, multi-column + date-range pushdown, IN (...), and the adjustments list-filter path.
  • Also verified the built wasm end-to-end against a Paddle sandbox account via cargo pgrx run, with api_url pointed at a small logging proxy to confirm the pushed-down URLs actually reach Paddle, e.g. GET /transactions?per_page=30&customer_id=...&created_at[GTE]=...&created_at[LT]=....
  • cargo fmt, cargo clippy -D warnings, and cargo component build are clean.

Everything went through Fable 5 adversarial review, too.

Other

  • Paddle's date filters are whole-second, so bounds are rounded to keep the pushed range a superset of the SQL qual (Postgres rechecks locally), meaning pushdown never drops rows.
  • The 0.3.0 checksum in the docs is a placeholder for now. I understand the real one comes from the release build.
  • I thought about adding nested customer subresources (e.g. addresses) but they require customer_id in the path. I'm going to take this back to the team and think about the pattern some more.
  • As before no delete support, since we typically only offer archiving via a status update (we're an MoR).

…hema

Improve the Paddle Wasm FDW (0.2.0 -> 0.3.0):

- Fix `subscriptions` id pushdown: `where id = '...'` now does a single-object
  GET instead of a full paginated scan.
- Add a multi-column + date-range qual pushdown framework (modeled on orb_fdw).
  Date-range operators ([LT]/[LTE]/[GT]/[GTE]) are transactions-only per the
  Paddle API; bounds use superset rounding so pushdown never drops rows.
  `IN (...)` lists are pushed as Paddle comma-separated values.
- Percent-encode pushed filter values and the id path segment.
- Fully wire `adjustments` (refunds/chargebacks); its `id` is a list filter
  since Paddle has no single-object adjustments endpoint.
- Expand `import foreign schema` to 7 richer, typed tables (customers, products,
  prices, subscriptions, transactions, discounts, adjustments) and honor
  `limit to` / `except`.
- Treat a column missing from the API response as SQL NULL rather than erroring.
- Update docs/catalog/paddle.md and add mock-backed tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@burmecia

Copy link
Copy Markdown
Member

Thanks for the PR @heymcgovern 👍! Can we add price_id and scheduled_change_action columns to the subscriptions generated import schema? They are in pushed-down filters but not in generated columns. Other than that, all LGTM.

…ptions import

The subscriptions generated import schema listed `price_id` and
`scheduled_change_action` as pushed-down filters but didn't create the columns,
so they weren't usable from the imported table. Add both (filter-oriented — the
underlying values are nested, so they read as NULL and the actual data is in
`attrs`). Addresses review feedback on supabase#616.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@heymcgovern

Copy link
Copy Markdown
Contributor Author

Done in 7bc90cc — added price_id and scheduled_change_action to the subscriptions import schema so the generated columns match the pushed-down filters. Heads-up: they're filter-oriented — Paddle nests the underlying values (items[].price.id and scheduled_change.action), so the columns themselves read as NULL and the data is available via attrs (noted in the entity docs). Happy to drop them from the filter list instead if you'd prefer.

🤖 Addressed by Claude Code

@heymcgovern

Copy link
Copy Markdown
Contributor Author

Just pulling back to draft for the mo. Claude was a bit keen here and I need to dig into this a bit more. I'm not sure scheduled_change_action works for filtering so I just need to check

@heymcgovern
heymcgovern marked this pull request as draft July 23, 2026 10:11
The previous commit added `price_id` and `scheduled_change_action` as
subscriptions columns, but both read NULL (the values are nested in the API
response). Because the wrapper's pushed-down quals are re-checked locally by
Postgres, filtering a NULL column silently returns zero rows.

- Populate `scheduled_change_action` from the nested `scheduled_change.action`
  (defaulting to 'none' when there's no pending change, matching the filter
  enum), so the filter works end-to-end. Gated to the subscriptions object.
- Remove `price_id` from the filter list, generated column, and docs: a
  subscription can have multiple item prices and Paddle's filter is an
  any-item match, so no scalar column can represent it under recheck. Prices
  remain available via `attrs->'items'`.
- Don't comma-join IN (...) for single-value params (collection_mode, type,
  mode), which Paddle would reject.
- Add a pushdown test for scheduled_change_action.

Addresses review feedback on supabase#616.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@heymcgovern

Copy link
Copy Markdown
Contributor Author

Ok thanks for your review here, and cheers for your patience while I looked into this. A couple of these params are new to me so I wanted to dig into the API properly.

  • scheduled_change_action didn't work as a plain col. The value comes back nested under scheduled_change.action, so the column was NULL. This means where scheduled_change_action = '…' was silently returning nothing. I fixed this by populating the column from the nested field (none when there's no pending change, matching the query param value), so filtering works properly now.
  • price_id - removed this entirely. A subscription can have multiple item prices (e.g. Pro plan + monthly addons) and the filter is an any-item match, so no single column can represent it correctly here. Prices are still available using attrs->'items'.

Tested and verified this all against my sandbox account. Thanks again 😄

@heymcgovern
heymcgovern marked this pull request as ready for review July 23, 2026 15:32

@burmecia burmecia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That looks great now! Thanks @heymcgovern !

@burmecia
burmecia merged commit db43ca0 into supabase:main Jul 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants