Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ts-pg-promise: typed queries for pg-promise without runtime dependencies #559

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

ml-mave
Copy link

@ml-mave ml-mave commented Jan 4, 2024

Hi! This PR is a proposal for a new transform mode in pgtyped ("ts-pg-promise") which adds support for generating types for pg-promise queries without requiring any runtime dependencies1. In our codebase, we use pg-promise a lot and wanted an easy way to get better type safety for SQL queries, and after a bit of research it turned out pgtyped was pretty close to what we would like to have.

The generator searches for tx.{none,one,oneOrNone,many,manyOrNone} calls in source files (where the name tx is configurable as an array of possible names) and inspects the first argument of the call.

  • If the query is a string literal, the query is parsed and processed as usual.
  • Otherwise, the generator invokes TS type checker to get the type of the argument.
    • If the type is a union of string literals, it finds the constituent with the largest number of parameters and uses that as the query text. The parameters of all other constituents must appear in the largest query; otherwise it wouldn't be possible to generate a sensible parameter type. If this condition is not satisfied, the query is skipped and a warning is printed.
    • Otherwise, the query is skipped and a warning is printed.

Queries are automatically named based on the name of the enclosing function. If a function contains several queries or if the function name is not unique across all source files, the conflicting names are disambiguated with a number.

The generator creates an interface containing method signatures for each query so that:

  • the first parameter type is the query literal (or a union of literals if the query is dynamic)
  • if the query has any parameters, the second parameter is the input parameter object type

The generator adds 5 fallback overloads to make error messages a little less bad (but they are still not great). Based on some testing, TypeScript always reports the last overload if the argument types don't match, so it would probably require changes to TS compiler itself to improve error messages. Ideally, the overload candidate should be the one whose first parameter passes type check.

Raw and identifier bindings (e.g. $(foo^), $(bar~)) are not supported (but they are still parsed so that a proper warning message is printed).

The generated parameter field types are non-optional and by default not nullable. It seems there was no existing mechanism for adjusting nullabilities of individual parameters, so for this mode we've added a magic comment /*nullable*/ so you can have for example:

SELECT foo
FROM bar
WHERE baz = $(baz)/*nullable*/

Configuration options for ts-pg-promise mode:

  • include: Glob pattern for source files to include
  • exclude: Glob pattern for source files to exclude
  • emitFileName: Path to output file
  • tsconfigPath: Path to tsconfig.json
  • maxMethodParameterUnionTypeLength (default: 10000): For dynamic queries, the union type can sometimes become very long because TS cross multiplies each union in an interpolated position in a template literal type. If the total length exceeds this value, the method signature is skipped and the result/parameter interface must be imported to the query manually.
  • variableNames: Array of variable names to search for in source files
  • interfaceName: Name of the generated interface that contains all the query method signatures
  • argumentTypeWarning (default: true): If set to true, a warning is printed if the type of the query argument is incompatible with type generation
  • parameterKindWarning (default: true): If set to true, a warning is printed if the query contains unsupported raw or identifier parameters

There's also some other new general configuration options:

  • enums.style (default: type): Style of enums to generate, can be enum to generate TS enums or type to generate a union of string literals. For enum style, a few additional options are available:
    • enums.nameCase: pascal to make enum name pascal case or keep to not transform the database name at all
    • enums.keyCase: upper, lower or sameAsValue
    • enums.dropNameSuffix: If the database enum name ends with this string, it is stripped off
  • interfaceComments (default: true): If set to false, skips generation of comments
  • db.schema: Database schema (search path)
  • anonymousColumnWarning (default: true): If set to false, suppresses warning about anonymous columns

Documentation and tests are incomplete/lacking, but we wanted to send this PR for initial feedback now. Sorry for not opening an issue first, and thanks for reading! =)

Footnotes

  1. The only required runtime change is that trailing ! or ? in result column names must be stripped off because pgtyped uses this syntax for adjusting result column nullabilities manually.

Copy link

vercel bot commented Mar 18, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
pgtyped ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 18, 2024 3:17pm

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.

1 participant