8000 feat(postgres): run queries on slonik by colinhacks · Pull Request #121 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

feat(postgres): run queries on slonik - #121

Merged
colinhacks merged 7 commits into
mainfrom
slonik-support
Oct 25, 2026
Merged

colinhacks merged 7 commits into
mainfrom
slonik-support

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

.run(), .with() and try() now run on a Slonik pool, a connection from pool.connect(), a transaction and a savepoint, with the values Reylo returns on every other driver. This replaces the refusal added in #91.

Slonik's query() runs only the queries its sql tag builds, and its type parsers are set on the pool's connections: with its defaults a numeric arrives as a float, a timestamp as epoch milliseconds and an interval as seconds. Slonik 49.12.0 lets a tag made by createSqlTag({ typeParsers }) override those parsers for its own queries (gajus/slonik#824). Reylo uses that, and changes nothing in the statement.

  • The tag. Reylo takes createSqlTag from the app's own slonik, through require reached with process.getBuiltinModule("node:module"), from the app's entry script and then its working directory. An import() drew Rspack's "Critical dependency" warning in every app, Slonik or not. The tag passes through as text every built-in type that pg (pg-types 4) or Slonik's preset converts, plus every type the app's pool names a parser for.
  • The statement. Each parameter appears once in a statement, so Reylo's lexer splits it at its placeholders into the tag's template: the text between them, and the values in their order. Literals and comments are untouched, and nothing goes through slonik-sql-tag-raw's raw(), which rebinds every $n and matches inside literals. A query defined by hand that uses a parameter twice is refused, since the tag would bind each use on its own.
  • The values. undefined binds NULL, binary data goes as \x hex text, and arrays and JSON as on pg, since the tag takes only strings, numbers, booleans, BigInts and null. Rows are decoded by each column's kind, or for a query defined by hand by its type's oid.
  • Refused, with Reylo's own error: a pool whose interceptor rewrites rows (slonik-interceptor-field-name-transformation), and a value a custom driver converted anyway, where it ignored the query's parsers.
  • try() reads pg's error through Slonik's wrapper (InputSyntaxError, UniqueIntegrityConstraintViolationError), which keeps it as cause.
  • slonik is an optional peer, >=49.12.0, and a dev dependency of the root, the driver matrix and the docs site.

Docs: a Slonik tab in .run()'s Postgres examples and transactions, a slonik section on the Postgres page, and slonik in the README's list.

Checked:

  • tests/representation.test.ts runs one row through a Slonik pool with Slonik's own parsers plus an app parser on uuid. The pool, a connection and a transaction return the same row as every other driver, and the app's own query keeps its parsers.
  • tests/execute.test.ts: the template, positional placeholders out of order, the values, a query defined by hand, and the three refusals.
  • The driver matrix's new job, slonik@49.12.0, passes 110 rows over the pool, a connection, a transaction and a savepoint, with exact values, round trips and try(). The 8 skips are the parameter-used-twice scenarios. A full run compares with RESULTS.md with 0 differences.
  • The matrix's type check accepts Slonik's pool, connection and transaction types for a Postgres query and rejects them for MySQL and SQLite.
  • The loader tests (none skipped), the package build, the bundler, Jest, Next.js and drivers labs, every example including Rspack and Cloudflare Workers, and the docs build pass.

Copilot AI balanced review requested due to automatic review settings October 25, 2026 03:48
@vercel
vercel Bot commented Oct 25, 2026 •
Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 25, 2026 4:03am UTC

Request Review

Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@pullfrog pullfrog 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.

Important

The interceptor refusal fires after Slonik has already run the statement, so a write can commit and still be reported to the app as an error. Details inline.

Reviewed changes

I reviewed the full diff. I also checked the parts of Slonik 49.12.0 the PR depends on against its published tarballs (slonik, @slonik/sql-tag, @slonik/pg-driver):

  • Slonik through its own tag: .run()'s Postgres branch now sends a Slonik pool, connection, transaction or savepoint to slonik() in src/decode.ts. That function builds a createSqlTag({ typeParsers }) tag whose parsers return each value unchanged, splits the $n text into the tag's template, and decodes the rows by column kind.
  • Loading slonik: the app's slonik is loaded with createRequire from argv[1], then from the working directory. Slonik 49 is ESM-only on Node 24 or later, so this relies on require(esm). Its tokens use Symbol.for, so a tag from a second copy of Slonik is still accepted.
  • Parameters: slonikParam turns each value into one the tag accepts: undefined becomes NULL, bytes become \x hex, arrays become literals, and a Date becomes its ISO string. A hand-written query that uses a parameter twice is refused.
  • Type parsers: Slonik's per-query parsers also cover each type's array type (typarray in @slonik/pg-driver), so numeric[] and timestamptz[] arrive as text, as the PR relies on.
  • try(): reads pg's error from the cause on Slonik's error wrapper.
  • Lab and docs: a driver-matrix job and type-check cases for slonik@49.12.0, plus docs, README, and an optional peer slonik >=49.12.0.

ℹ️ Apps that bundle slonik cannot run Reylo's queries on it

loadSlonik can only find slonik as a package on disk, starting from argv[1] or the working directory. An app that bundles slonik into its server output and ships no node_modules (an esbuild-bundled Lambda, for example) fails every Slonik query with the "could not load" TypeError, even though the pool works. The new docs section on the Postgres page doesn't mention this.

Technical details
# A bundled slonik is not resolvable by loadSlonik

## Affected sites
- src/decode.ts:443-458 — `loadSlonik` resolves "slonik" only from disk (`createRequire(argv[1] | cwd)`)
- site/content/docs/postgres.mdx:924 — the slonik section lists requirements but not this one

## Required outcome
- A user whose deployment bundles slonik learns before deploying that slonik must be left external, so it can be resolved at run time.

## Open questions for the human
- Is a docs note enough, or should the refusal message suggest marking slonik external in the bundler?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread src/decode.ts Resolved
@colinhacks
Copy link
Copy Markdown
Owner Author

On bundling slonik (from the review summary): addressed in 0ad50b8. The slonik section on the Postgres page now says that Reylo makes its tag with the app's own slonik, loaded at run time from node_modules, so a bundled server keeps slonik external. The "could not load" error says the same thing, so an app that bundled it learns the fix from the error.

@pullfrog pullfrog 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 prior finding is fixed. One small gap is left, noted inline.

Reviewed changes

I reviewed 0ad50b8 against the prior review at 6678654, and checked it against executeQuery.js in Slonik 49.12.0.

  • Up-front interceptor refusal: slonik() now refuses a client whose interceptors include a transformRow before query() is called. It reads the interceptors from configuration (a pool) or clientConfiguration (a connection, transaction or savepoint). The check on the rows' keys after the query is gone.
  • Tests: the refusal test puts the interceptor in both places and asserts that nothing was sent. It also checks that an interceptor with only afterQueryExecution still runs, which is right: Slonik ignores what afterQueryExecution and beforeQueryResult return.
  • Bundled servers: the docs and the "could not load" error now say that a bundled server must leave slonik external. This covers the earlier informational note.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread src/decode.ts Resolved
@pullfrog pullfrog 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.

✅ No new issues found. The last open finding is fixed.

Reviewed changes

I reviewed 57d576d against the prior review at 0ad50b8.

  • transformRowAsync refused too: slonik() now refuses, before query() runs, a client that has an interceptor with either transformRow or transformRowAsync. It checks both configuration and clientConfiguration. In Slonik 49.12.0's executeQuery.js, these are the only two hooks that replace result.rows.
  • Error and docs: the refuseSlonikRows message and the interceptor sentence in postgres.mdx now name both hooks.
  • Test: the refusal test now runs both hooks on both config locations and still checks that nothing was sent.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit 349e953 into main Oct 25, 2026
7 checks passed
@colinhacks
colinhacks deleted the slonik-support branch October 25, 2026 04:05
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

0