<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:fh="http://purl.org/syndication/history/1.0"><channel><title>UQL | Blog</title><description>A TypeScript ORM whose queries are plain JSON values: typed to the leaf, no codegen, and one API across Postgres, CockroachDB, MySQL, SQLite, MongoDB, and the edge.</description><link>https://uql-orm.dev</link><language>en</language><fh:complete/><atom:link rel="self" href="https://uql-orm.dev/blog/rss.xml"/><item><title>The SQL tag: a function called without parentheses</title><link>https://uql-orm.dev/blog/sql-tag</link><guid isPermaLink="true">https://uql-orm.dev/blog/sql-tag</guid><description>In UQL 0.100.0, all and run are tags. The missing parentheses are what keep your values out of the SQL.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Here is a line from UQL v0.100.x:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;`SELECT * FROM &quot;User&quot; WHERE &quot;email&quot; = ${&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;No parentheses, yet this is still a function call. How JavaScript makes that call is what keeps (vanilla) &lt;code dir=&quot;auto&quot;&gt;email&lt;/code&gt; out of the SQL; yes, it will be automatically sanitized. “But how?” you may ask, and here is the answer below.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-tag-receives&quot;&gt;What a tag receives&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A template literal with a function name is a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates&quot;&gt;tagged template&lt;/a&gt;. It is plain JavaScript, there since
ES2015, so it runs in every browser, Node and Bun with no build step.&lt;/p&gt;
&lt;p&gt;The JS engine calls the tag with the template cut into pieces: first an array of the literal text, then each &lt;code dir=&quot;auto&quot;&gt;${}&lt;/code&gt;
value as an argument of its own. A tag that prints its arguments shows the split.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;show&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;texts&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TemplateStringsArray&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;values&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;[]) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(texts, values);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;show&lt;/span&gt;&lt;span&gt;`SELECT * FROM &quot;User&quot; WHERE &quot;email&quot; = ${&apos;ana@example.com&apos;} AND &quot;age&quot; &gt; ${&lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// [&apos;SELECT * FROM &quot;User&quot; WHERE &quot;email&quot; = &apos;, &apos; AND &quot;age&quot; &gt; &apos;, &apos;&apos;]  [&apos;ana@example.com&apos;, 18]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;There is always one more string than there are values; here the last one is empty.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;from-pieces-to-a-query&quot;&gt;From pieces to a query&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The text pieces come from your (static) source code. The values are dynamic, and can come from anywhere (users, other apps, microservices, etc.) at run time. A database driver mandates
exactly that split: SQL with placeholders, and the parameters beside it.&lt;/p&gt;
&lt;p&gt;So this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;texts&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TemplateStringsArray&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;values&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;[]) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; texts.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;part&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`${&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;}$${&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;}${&lt;/span&gt;&lt;span&gt;part&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; { text, values };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`SELECT * FROM &quot;User&quot; WHERE &quot;email&quot; = ${&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Produces this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;SELECT * FROM &quot;User&quot; WHERE &quot;email&quot; = $1&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;values&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&apos;ana@example.com&apos;&lt;/span&gt;&lt;span&gt;] }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Whatever &lt;code dir=&quot;auto&quot;&gt;email&lt;/code&gt; holds, even a SQL injection attempt as &lt;code dir=&quot;auto&quot;&gt;&apos;; DROP TABLE &quot;User&quot;; --&lt;/code&gt;, is sent as the value of &lt;code dir=&quot;auto&quot;&gt;$1&lt;/code&gt;, never as SQL. No manual escaping needed, no SQL injection.&lt;/p&gt;
&lt;p&gt;In UQL the method is the tag: &lt;code dir=&quot;auto&quot;&gt;all&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;run&lt;/code&gt; take the template directly, so there is no &lt;code dir=&quot;auto&quot;&gt;sql&lt;/code&gt; helper to import and wrap.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-parentheses-trap&quot;&gt;The parentheses trap&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Add parentheses and you have a different program:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;`SELECT * FROM &quot;User&quot; WHERE &quot;email&quot; = ${&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// @ts-expect-error a plain string, with the value already inside it&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`SELECT * FROM &quot;User&quot; WHERE &quot;email&quot; = ${&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The second line builds the whole string first, &lt;code dir=&quot;auto&quot;&gt;email&lt;/code&gt; and all, and hands the function the finished text. No
library can take the value back out. That is why, in UQL, that last line does not compile.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-typescript-adds&quot;&gt;What TypeScript adds&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;TypeScript checks each &lt;code dir=&quot;auto&quot;&gt;${}&lt;/code&gt; against the tag’s parameter types, so UQL can say
what a database can bind: strings, numbers, booleans, &lt;code dir=&quot;auto&quot;&gt;bigint&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Date&lt;/code&gt;, bytes, &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt;, lists of those, and SQL
fragments. Two common slips stop at the compiler.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// @ts-expect-error `status` may be undefined, which binds nothing&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;`SELECT * FROM &quot;User&quot; WHERE &quot;status&quot; = ${&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// @ts-expect-error an object is not a SQL value&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;`UPDATE &quot;User&quot; SET &quot;prefs&quot; = ${&lt;/span&gt;&lt;span&gt;prefs&lt;/span&gt;&lt;span&gt;} WHERE &quot;id&quot; = ${&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// this one compiles&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;`UPDATE &quot;User&quot; SET &quot;prefs&quot; = ${&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;prefs&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;} WHERE &quot;id&quot; = ${&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A tag also takes a type argument, so &lt;code dir=&quot;auto&quot;&gt;pool.all&amp;#x3C;{ id: number; email: string }&gt;`...` &lt;/code&gt; hands back typed rows.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;fragments&quot;&gt;Fragments&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;raw&lt;/code&gt; is the same tag without running anything: it returns a fragment. A fragment inside another template is
spliced in, its values still bound:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { raw } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;uql-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;conditions&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;raw&lt;/span&gt;&lt;span&gt;`&quot;createdAt&quot; &gt; ${&lt;/span&gt;&lt;span&gt;since&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (status) conditions.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;raw&lt;/span&gt;&lt;span&gt;`&quot;status&quot; = ${&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;`SELECT * FROM &quot;User&quot; WHERE ${&lt;/span&gt;&lt;span&gt;raw&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;conditions&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos; AND &apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For trusted SQL held in a string, such as a file’s contents, use &lt;code dir=&quot;auto&quot;&gt;raw.text(sql)&lt;/code&gt;. It is the only call that binds
nothing, so it is easy to find in a review. A tag binds values, not names: a table or column taken from user input still needs checking against a list you control.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;also-in-01000&quot;&gt;Also in 0.100.0&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;undefined-in-a-filter-throws&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;undefined&lt;/code&gt; in a filter throws&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Prisma’s docs warn that &lt;code dir=&quot;auto&quot;&gt;deleteMany({ where: { id: undefined } })&lt;/code&gt; &lt;a href=&quot;https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined&quot;&gt;deletes
every user&lt;/a&gt;, because an
undefined key is dropped from the filter; UQL used to drop it too, but now it throws:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;deleteMany&lt;/span&gt;&lt;span&gt;(User, { $where: { email: maybeEmail } });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// UqlUsageError: $where on &apos;User&apos; holds undefined at &apos;email&apos;: leave the key out not to filter by it, or name null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;writes-that-do-what-the-code-says&quot;&gt;Writes that do what the code says&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A &lt;code dir=&quot;auto&quot;&gt;transaction()&lt;/code&gt; inside another is a savepoint, so a failing inner step rolls back alone.
A to-many update keeps the children the payload lists and removes the rest. Any write can take
&lt;a href=&quot;https://uql-orm.dev/querying/methods#returning&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;returning&lt;/code&gt;&lt;/a&gt; and resolve to the rows it wrote, typed by the fields you name.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;by-the-way-how-to-quickly-move-to-all-this&quot;&gt;By the way, how to quickly move to all this?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;npx uql-codemod&lt;/code&gt; updates your imports and turns &lt;code dir=&quot;auto&quot;&gt;all(sql, [a, b])&lt;/code&gt; into a tag. Anything it cannot convert safely
is listed for you. The &lt;a href=&quot;https://uql-orm.dev/upgrade-guide&quot;&gt;upgrade guide&lt;/a&gt; has the rest.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>typescript</category><category>sql</category><category>orm</category></item><item><title>Type-safe triggers in an ORM. What?</title><link>https://uql-orm.dev/blog/type-safe-triggers</link><guid isPermaLink="true">https://uql-orm.dev/blog/type-safe-triggers</guid><description>A lifecycle hook only runs on the ORM&apos;s own writes. A trigger runs on every write, and in UQL its body is checked by the compiler and rendered for each SQL engine.</description><pubDate>Sat, 26 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An audit log written by a lifecycle hook passes every test. Then someone runs a backfill, fixes a row in
&lt;code dir=&quot;auto&quot;&gt;psql&lt;/code&gt;, or points a second service at the table, and the log has a gap. Nothing failed: an ORM hook only
sees the ORM’s own writes.&lt;/p&gt;
&lt;p&gt;A database trigger sees every write. In UQL you declare it on the entity, the compiler checks its body, and
uql renders it for each SQL engine.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-audit-log-on-the-entity&quot;&gt;The audit log, on the entity&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Triggers usually live as SQL strings in an old migration: invisible from the model, unchecked against the
columns, and written again for every engine. Here the trigger sits beside the table it watches:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Entity, Field, Id, insertInto, Trigger } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;uql-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Entity&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PostAudit&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;span&gt;({ type: Number })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: Number, name: &lt;/span&gt;&lt;span&gt;&apos;post_id&apos;&lt;/span&gt;&lt;span&gt;, nullable: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;postId&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;id&apos;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: String, name: &lt;/span&gt;&lt;span&gt;&apos;from_status&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;fromStatus&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;status&apos;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: String, name: &lt;/span&gt;&lt;span&gt;&apos;to_status&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;toStatus&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;status&apos;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Trigger&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;on: &lt;/span&gt;&lt;span&gt;&apos;afterUpdate&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;audit&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; [post.status],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;newRow&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;oldRow&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;insertInto&lt;/span&gt;&lt;span&gt;(PostAudit, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;postId: newRow.id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fromStatus: oldRow.status,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;toStatus: newRow.status,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Entity&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;span&gt;({ type: Number })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: String })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;of&lt;/code&gt; fires the trigger only when &lt;code dir=&quot;auto&quot;&gt;status&lt;/code&gt; changes, and &lt;code dir=&quot;auto&quot;&gt;run&lt;/code&gt; writes the audit row. Columns are read as
properties, so the compiler knows every name and type. A &lt;code dir=&quot;auto&quot;&gt;where&lt;/code&gt; adds conditions on either row, such as
&lt;code dir=&quot;auto&quot;&gt;{ $old: { status: &apos;draft&apos; } }&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-compiler-catches&quot;&gt;What the compiler catches&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Each of these fails at build time, not when the trigger first fires:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { defineTrigger, insertInto } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;uql-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;defineTrigger&lt;/span&gt;&lt;span&gt;(Post, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;on: &lt;/span&gt;&lt;span&gt;&apos;afterInsert&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;_newRow&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;oldRow&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;insertInto&lt;/span&gt;&lt;span&gt;(PostAudit, { postId: oldRow.id }), &lt;/span&gt;&lt;span&gt;// error: no old row on insert&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;defineTrigger&lt;/span&gt;&lt;span&gt;(Post, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;on: &lt;/span&gt;&lt;span&gt;&apos;afterUpdate&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;newRow&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;insertInto&lt;/span&gt;&lt;span&gt;(PostAudit, { post: newRow.id }), &lt;/span&gt;&lt;span&gt;// error: PostAudit has no `post`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;defineTrigger&lt;/span&gt;&lt;span&gt;(Post, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;on: &lt;/span&gt;&lt;span&gt;&apos;afterUpdate&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;newRow&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;insertInto&lt;/span&gt;&lt;span&gt;(PostAudit, { postId: newRow.status }), &lt;/span&gt;&lt;span&gt;// error: postId holds a number&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;So does watching a column the entity lacks, or an &lt;code dir=&quot;auto&quot;&gt;updateTable&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;deleteFrom&lt;/code&gt; with no &lt;code dir=&quot;auto&quot;&gt;$where&lt;/code&gt;, which
would hit every row on every fire.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-declaration-every-sql-engine&quot;&gt;One declaration, every SQL engine&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is what &lt;code dir=&quot;auto&quot;&gt;sync&lt;/code&gt; and generated migrations install. CockroachDB gets the Postgres form, MariaDB the MySQL
one:&lt;/p&gt;
&lt;div&gt;&lt;starlight-tabs data-sync-key=&quot;dialect&quot;&gt;&lt;div&gt;&lt;ul role=&quot;tablist&quot;&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-0-0&quot; id=&quot;tab-0-0&quot; aria-selected=&quot;true&quot; tabindex=&quot;0&quot;&gt;PostgreSQL&lt;/a&gt;&lt;/li&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-0-1&quot; id=&quot;tab-0-1&quot; aria-selected=&quot;false&quot; tabindex=&quot;-1&quot;&gt;MySQL&lt;/a&gt;&lt;/li&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-0-2&quot; id=&quot;tab-0-2&quot; aria-selected=&quot;false&quot; tabindex=&quot;-1&quot;&gt;SQLite&lt;/a&gt;&lt;/li&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-0-3&quot; id=&quot;tab-0-3&quot; aria-selected=&quot;false&quot; tabindex=&quot;-1&quot;&gt;SQL Server&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-0-0&quot; aria-labelledby=&quot;tab-0-0&quot; role=&quot;tabpanel&quot;&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE OR REPLACE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FUNCTION&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;_uql_Post__audit_0a57e9&lt;/span&gt;&lt;span&gt;&quot;() &lt;/span&gt;&lt;span&gt;RETURNS&lt;/span&gt;&lt;span&gt; trigger &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; $uql$&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;BEGIN&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;PostAudit&quot;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;&quot;post_id&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;from_status&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;to_status&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (NEW.&lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;, OLD.&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;, NEW.&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;RETURN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt; $uql$ &lt;/span&gt;&lt;span&gt;LANGUAGE&lt;/span&gt;&lt;span&gt; plpgsql;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TRIGGER&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;_uql_Post__audit_0a57e9&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AFTER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; OF &lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Post&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FOR&lt;/span&gt;&lt;span&gt; EACH &lt;/span&gt;&lt;span&gt;ROW&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WHEN&lt;/span&gt;&lt;span&gt; (OLD.&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DISTINCT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; NEW.&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;EXECUTE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FUNCTION&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;_uql_Post__audit_0a57e9&quot;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-0-1&quot; aria-labelledby=&quot;tab-0-1&quot; role=&quot;tabpanel&quot; hidden&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TRIGGER&lt;/span&gt;&lt;span&gt; `&lt;/span&gt;&lt;span&gt;_uql_Post__audit_94c077&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AFTER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`Post`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FOR&lt;/span&gt;&lt;span&gt; EACH &lt;/span&gt;&lt;span&gt;ROW&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;BEGIN&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOT&lt;/span&gt;&lt;span&gt; (OLD.&lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;=&gt;&lt;/span&gt;&lt;span&gt; NEW.&lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;THEN&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`PostAudit`&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;`post_id`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`from_status`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`to_status`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (NEW.&lt;/span&gt;&lt;span&gt;`id`&lt;/span&gt;&lt;span&gt;, OLD.&lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt;, NEW.&lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;END&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-0-2&quot; aria-labelledby=&quot;tab-0-2&quot; role=&quot;tabpanel&quot; hidden&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TRIGGER&lt;/span&gt;&lt;span&gt; `&lt;/span&gt;&lt;span&gt;_uql_Post__audit_08d35c&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AFTER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; OF &lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`Post`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FOR&lt;/span&gt;&lt;span&gt; EACH &lt;/span&gt;&lt;span&gt;ROW&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WHEN&lt;/span&gt;&lt;span&gt; (OLD.&lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOT&lt;/span&gt;&lt;span&gt; NEW.&lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;BEGIN&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`PostAudit`&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;`post_id`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`from_status`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`to_status`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (NEW.&lt;/span&gt;&lt;span&gt;`id`&lt;/span&gt;&lt;span&gt;, OLD.&lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt;, NEW.&lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;END&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-0-3&quot; aria-labelledby=&quot;tab-0-3&quot; role=&quot;tabpanel&quot; hidden&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TRIGGER&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;_uql_Post__audit_d8fbff&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Post&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AFTER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;BEGIN&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOCOUNT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;PostAudit&quot;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;&quot;post_id&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;from_status&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;to_status&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; inserted.&lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;, deleted.&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;, inserted.&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; inserted &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; deleted &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; inserted.&lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; deleted.&lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EXISTS&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; deleted.&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EXCEPT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; inserted.&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;END&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;starlight-tabs-restore&gt;&lt;/starlight-tabs-restore&gt;&lt;/starlight-tabs&gt;&lt;/div&gt;
&lt;p&gt;“Fire only when &lt;code dir=&quot;auto&quot;&gt;status&lt;/code&gt; changes” has to count &lt;code dir=&quot;auto&quot;&gt;NULL&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;&apos;draft&apos;&lt;/code&gt; as a change, which a plain &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;&gt;&lt;/code&gt; does not.
Each engine spells that differently, and SQL Server hands the trigger tables rather than rows: four chances
to get it subtly wrong by hand.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-other-orms-do-it&quot;&gt;How other ORMs do it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The same trigger, on PostgreSQL only:&lt;/p&gt;
&lt;div&gt;&lt;starlight-tabs data-sync-key=&quot;orm&quot;&gt;&lt;div&gt;&lt;ul role=&quot;tablist&quot;&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-1-0&quot; id=&quot;tab-1-0&quot; aria-selected=&quot;true&quot; tabindex=&quot;0&quot;&gt;Drizzle, Prisma, TypeORM&lt;/a&gt;&lt;/li&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-1-1&quot; id=&quot;tab-1-1&quot; aria-selected=&quot;false&quot; tabindex=&quot;-1&quot;&gt;MikroORM&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-1-0&quot; aria-labelledby=&quot;tab-1-0&quot; role=&quot;tabpanel&quot;&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- No trigger API. This goes in a hand-written migration: `drizzle-kit generate --custom`,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- `prisma migrate dev --create-only`, or `queryRunner.query(...)` in a TypeORM migration.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FUNCTION&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;post_audit&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;RETURNS&lt;/span&gt;&lt;span&gt; trigger &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; $$&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;BEGIN&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; post_audit (post_id, from_status, to_status)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;NEW&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;OLD&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;NEW&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;RETURN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt; $$ &lt;/span&gt;&lt;span&gt;LANGUAGE&lt;/span&gt;&lt;span&gt; plpgsql;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TRIGGER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;post_audit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AFTER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; OF &lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FOR&lt;/span&gt;&lt;span&gt; EACH &lt;/span&gt;&lt;span&gt;ROW&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WHEN&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;OLD&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DISTINCT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NEW&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;EXECUTE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FUNCTION&lt;/span&gt;&lt;span&gt; post_audit();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-1-1&quot; aria-labelledby=&quot;tab-1-1&quot; role=&quot;tabpanel&quot; hidden&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;MikroORM&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Declared on the entity and installed by migrations. The callback maps property names to&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// column names; the body and `when` around them are SQL strings, one set per dialect.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { defineEntity, p } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;@mikro-orm/core&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PostSchema&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defineEntity&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;Post&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;properties: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id: p.&lt;/span&gt;&lt;span&gt;integer&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;primary&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;status: p.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;nullable&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;triggers: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;post_audit&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;timing: &lt;/span&gt;&lt;span&gt;&apos;after&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;events: [&lt;/span&gt;&lt;span&gt;&apos;update&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;when: &lt;/span&gt;&lt;span&gt;&apos;OLD.status IS DISTINCT FROM NEW.status&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;`INSERT INTO post_audit (post_id, from_status, to_status)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;VALUES (NEW.${&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}, OLD.${&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;}, NEW.${&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;}); RETURN NULL;`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;starlight-tabs-restore&gt;&lt;/starlight-tabs-restore&gt;&lt;/starlight-tabs&gt;&lt;/div&gt;
&lt;p&gt;MikroORM comes closest: the trigger lives on the entity and takes column names from its metadata, but the
SQL around them is a string, unchecked and written once per engine. TypeORM’s subscribers, like hooks, only
see TypeORM’s own writes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;keeping-them-in-sync&quot;&gt;Keeping them in sync&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;sync&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;generate:entities&lt;/code&gt; install what an entity declares and drop what it no longer does. Each name
ends in a hash of its SQL, so an unchanged trigger is left alone and an edited one is replaced. Triggers uql
did not create are never touched.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-it-stops&quot;&gt;Where it stops&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MongoDB has no triggers&lt;/strong&gt;, so a write to an entity declaring one is refused rather than made without it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;raw&lt;/code&gt; bodies are not translated&lt;/strong&gt;: write one that runs on every engine you use, or one per engine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entity filters do not apply inside a trigger&lt;/strong&gt;: soft delete and &lt;code dir=&quot;auto&quot;&gt;security&lt;/code&gt; do not scope its writes, so
scope them by the row’s own columns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The engine-specific rules are on the &lt;a href=&quot;https://uql-orm.dev/entities/triggers&quot;&gt;Triggers&lt;/a&gt; page.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-it-matters-with-coding-agents&quot;&gt;Why it matters with coding agents&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Agents write much of the code that touches a database, including what hooks never see: a one-off script, a
backfill, a quick &lt;code dir=&quot;auto&quot;&gt;UPDATE&lt;/code&gt;. A rule the database enforces holds for all of it, and a trigger on the entity
sits in the file the agent reads, checked by the compiler it runs.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;hook-trigger-or-stamp&quot;&gt;Hook, trigger or stamp&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logic around UQL’s own writes: call an API, publish an event, validate&lt;/td&gt;
&lt;td&gt;a &lt;a href=&quot;https://uql-orm.dev/entities/lifecycle-hooks&quot;&gt;lifecycle hook&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A row the database writes whoever changes the table, like an audit&lt;/td&gt;
&lt;td&gt;a &lt;a href=&quot;https://uql-orm.dev/entities/triggers&quot;&gt;trigger&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A column the database keeps current on every write, like &lt;code dir=&quot;auto&quot;&gt;updated&lt;wbr&gt;At&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a &lt;a href=&quot;https://uql-orm.dev/entities/computed-fields#stamps-stored-on-an-event&quot;&gt;stamp&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A stamp is a trigger uql writes the body of. &lt;code dir=&quot;auto&quot;&gt;currentTimestamp&lt;/code&gt; is the database clock, in UTC to the
millisecond on every engine:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { currentTimestamp, Entity, Field, Id } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;uql-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Entity&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;span&gt;({ type: Number })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: Date, computed: currentTimestamp, stored: [&lt;/span&gt;&lt;span&gt;&apos;update&apos;&lt;/span&gt;&lt;span&gt;] })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;updatedAt&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get started&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/entities/triggers&quot;&gt;Triggers&lt;/a&gt;&lt;/strong&gt;: every option, the SQL per engine, and the engine notes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/comparison&quot;&gt;Comparison&lt;/a&gt;&lt;/strong&gt;: triggers and the rest of the feature matrix, ORM by ORM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rogerpadilla/uql&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a trigger you need cannot be declared this way, open an issue with the SQL you would have written.&lt;/p&gt;</content:encoded><category>types</category><category>typescript</category><category>orm</category></item><item><title>What a rename breaks: six TypeScript ORMs</title><link>https://uql-orm.dev/blog/what-a-rename-breaks</link><guid isPermaLink="true">https://uql-orm.dev/blog/what-a-rename-breaks</guid><description>I renamed a field, a foreign key and a relation with each ORM&apos;s own language server. Four of the six left stale names behind that still compile.</description><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You pick a TypeScript ORM also for the type safety, right? Then press F2, the editor renames what it can see (automatically), and
the rest of the schema quietly keeps the old name (unsynced).&lt;/p&gt;
&lt;p&gt;One model, 19 places that name three of its members: indexes, both sides of a relation, a check
constraint, a projection, raw SQL in a filter.&lt;/p&gt;
&lt;p&gt;I pressed F2 on &lt;code&gt;emailAddress&lt;/code&gt; to &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;employerId&lt;/code&gt; to &lt;code&gt;workplaceId&lt;/code&gt;, &lt;code&gt;employer&lt;/code&gt; to &lt;code&gt;workplace&lt;/code&gt; in each and
counted what got left behind.&lt;/p&gt;
&lt;figure data-orm-demo=&quot;rename-safety&quot;&gt;&lt;div&gt;&lt;nav aria-label=&quot;Choose an ORM&quot;&gt;&lt;button type=&quot;button&quot; data-orm-entry=&quot;Drizzle&quot; aria-pressed=&quot;true&quot; title=&quot;Drizzle: 14 of 19 mentions the rename edited&quot; aria-label=&quot;Drizzle: 14 of 19 mentions the rename edited&quot; disabled&gt;&lt;span&gt;Drizzle&lt;/span&gt;&lt;span&gt;&lt;span data-orm-score=&quot;14&quot;&gt;14&lt;/span&gt;/19&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; data-orm-entry=&quot;MikroORM&quot; aria-pressed=&quot;false&quot; title=&quot;MikroORM: 3 of 19 mentions the rename edited&quot; aria-label=&quot;MikroORM: 3 of 19 mentions the rename edited&quot; disabled&gt;&lt;span&gt;MikroORM&lt;/span&gt;&lt;span&gt;&lt;span data-orm-score=&quot;3&quot;&gt;3&lt;/span&gt;/19&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; data-orm-entry=&quot;Prisma&quot; aria-pressed=&quot;false&quot; title=&quot;Prisma: 4 of 19 mentions the rename edited&quot; aria-label=&quot;Prisma: 4 of 19 mentions the rename edited&quot; disabled&gt;&lt;span&gt;Prisma&lt;/span&gt;&lt;span&gt;&lt;span data-orm-score=&quot;4&quot;&gt;4&lt;/span&gt;/19&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; data-orm-entry=&quot;Sequelize&quot; aria-pressed=&quot;false&quot; title=&quot;Sequelize: 4 of 19 mentions the rename edited&quot; aria-label=&quot;Sequelize: 4 of 19 mentions the rename edited&quot; disabled&gt;&lt;span&gt;Sequelize&lt;/span&gt;&lt;span&gt;&lt;span data-orm-score=&quot;4&quot;&gt;4&lt;/span&gt;/19&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; data-orm-entry=&quot;TypeORM&quot; aria-pressed=&quot;false&quot; title=&quot;TypeORM: 12 of 19 mentions the rename edited&quot; aria-label=&quot;TypeORM: 12 of 19 mentions the rename edited&quot; disabled&gt;&lt;span&gt;TypeORM&lt;/span&gt;&lt;span&gt;&lt;span data-orm-score=&quot;12&quot;&gt;12&lt;/span&gt;/19&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; data-orm-entry=&quot;UQL&quot; aria-pressed=&quot;false&quot; title=&quot;UQL: 19 of 19 mentions the rename edited&quot; aria-label=&quot;UQL: 19 of 19 mentions the rename edited&quot; disabled&gt;&lt;span&gt;UQL&lt;/span&gt;&lt;span&gt;&lt;span data-orm-score=&quot;19&quot;&gt;19&lt;/span&gt;/19&lt;/span&gt;&lt;/button&gt;&lt;/nav&gt;&lt;/div&gt;&lt;div&gt;&lt;button type=&quot;button&quot; data-rsd-replay=&quot;&quot; disabled&gt;Replay the rename&lt;/button&gt;&lt;ul&gt;&lt;li&gt;renamed&lt;/li&gt;&lt;li&gt;stopped by the compiler&lt;/li&gt;&lt;li&gt;left behind silently&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;div data-orm-fallback=&quot;&quot;&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { count, eq, relations, &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SQL, sql } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;drizzle-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; { NodePgDatabase } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;drizzle-orm/node-postgres&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;check,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;index,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;integer,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;pgTable,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;serial,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;text,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;uniqueIndex,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;drizzle-orm/pg-core&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;companies&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pgTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Company&apos;&lt;/span&gt;&lt;span&gt;, { id: &lt;/span&gt;&lt;span&gt;serial&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;primaryKey&lt;/span&gt;&lt;span&gt;() });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pgTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&apos;User&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id: &lt;/span&gt;&lt;span&gt;serial&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;primaryKey&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;emailAddress: &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;notNull&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;employerId: &lt;/span&gt;&lt;span&gt;integer&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;references&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; companies.id),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Generated column&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;emailLower: &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;generatedAlwaysAs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`lower(${&lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;emailAddress&lt;/span&gt;&lt;span&gt;})`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Index on the field&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(t.emailAddress),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Composite unique index&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;uniqueIndex&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(t.emailAddress, t.employerId),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Covering index column | n/a: Drizzle&apos;s index builder has no INCLUDE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Expression index&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`lower(${&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;emailAddress&lt;/span&gt;&lt;span&gt;})`&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Partial index condition&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(t.employerId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`${&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;emailAddress&lt;/span&gt;&lt;span&gt;} &amp;#x3C;&gt; &apos;&apos;`&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Check constraint&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;check&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;address_present&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`${&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;emailAddress&lt;/span&gt;&lt;span&gt;} &amp;#x3C;&gt; &apos;&apos;`&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;usersRelations&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;relations&lt;/span&gt;&lt;span&gt;(users, ({ &lt;/span&gt;&lt;span&gt;one&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Foreign key of a relation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;employer: &lt;/span&gt;&lt;span&gt;one&lt;/span&gt;&lt;span&gt;(companies, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fields: [users.employerId],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;references: [companies.id],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;companiesRelations&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;relations&lt;/span&gt;&lt;span&gt;(companies, ({ &lt;/span&gt;&lt;span&gt;many&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Inverse side of a relation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;staff: &lt;/span&gt;&lt;span&gt;many&lt;/span&gt;&lt;span&gt;(users),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;schema&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { companies, users, usersRelations, companiesRelations };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;declare&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NodePgDatabase&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; schema&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Field in the projection&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;({ id: users.id, address: users.emailAddress }).&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(users);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Field in the filter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(users).&lt;/span&gt;&lt;span&gt;where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(users.emailAddress, &lt;/span&gt;&lt;span&gt;&apos;ada@example.com&apos;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Field in the sort&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(users).&lt;/span&gt;&lt;span&gt;orderBy&lt;/span&gt;&lt;span&gt;(users.emailAddress);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Field inside a loaded relation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.query.companies.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;with: { staff: { columns: { emailAddress: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; } } },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Relation loaded by name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.query.users.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;({ with: { employer: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; } });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Field in inserted data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;(users).&lt;/span&gt;&lt;span&gt;values&lt;/span&gt;&lt;span&gt;({ emailAddress: &lt;/span&gt;&lt;span&gt;&apos;ada@example.com&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Field in updated data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;(users)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;({ emailAddress: &lt;/span&gt;&lt;span&gt;&apos;ada@example.com&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(users.id, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Foreign key in a grouped count&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;({ company: users.employerId, total: &lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;() })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(users)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;groupBy&lt;/span&gt;&lt;span&gt;(users.employerId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Field read off the result&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(users);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;address&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; user.emailAddress;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Raw SQL in a filter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(users)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`lower(${&lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;emailAddress&lt;/span&gt;&lt;span&gt;}) = ${&apos;ada@example.com&apos;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;div data-orm-editor=&quot;&quot; hidden&gt;&lt;/div&gt;&lt;/div&gt;&lt;figcaption&gt;&lt;span data-orm-file=&quot;&quot;&gt;rename-safety/drizzle.ts&lt;/span&gt; · &lt;span data-orm-status=&quot;&quot;&gt;static preview&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;div&gt;&lt;h2 id=&quot;two-ways-to-miss&quot;&gt;Two ways to miss&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A name the rename missed fails in one of two ways (and they’re not the same problem).&lt;/p&gt;
&lt;p&gt;Some stop compiling. That costs an afternoon: &lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt; hands you the list, you work through it, nothing reaches
production.&lt;/p&gt;
&lt;p&gt;The rest compile. The build is green, the tests that don’t touch that query pass, and the failure turns up
later as a column that isn’t there.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ORM&lt;/th&gt;
&lt;th&gt;Followed the rename&lt;/th&gt;
&lt;th&gt;Stopped by the compiler&lt;/th&gt;
&lt;th&gt;Left behind silently&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Rows don’t all add up to 19: where an ORM can’t express a probe, there’s nothing to rename and
nothing to count.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;dont-read-the-first-column-on-its-own&quot;&gt;Don’t read the first column on its own&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;MikroORM follows the fewest renames of the six, yet it is among the safest.&lt;/p&gt;
&lt;p&gt;Its &lt;code dir=&quot;auto&quot;&gt;defineEntity&lt;/code&gt; builder types index properties against the entity, so the stale name in
&lt;code dir=&quot;auto&quot;&gt;{ properties: [&apos;emailAddress&apos;] }&lt;/code&gt; is a compile error. 13 of its misses stop the
build, 3 get through. Go by the leftmost column and you’d worry about the wrong
tool. I did, for about a day.&lt;/p&gt;
&lt;p&gt;Sequelize is the one to worry about. Its classes are properly typed and &lt;code dir=&quot;auto&quot;&gt;InferAttributes&lt;/code&gt; does real work on
the model, but everything around the model takes strings, and nothing checks them:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Sequelize&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// an index on a column that will not exist&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;User.&lt;/span&gt;&lt;span&gt;init&lt;/span&gt;&lt;span&gt;(attributes, { sequelize, indexes: [{ fields: [&lt;/span&gt;&lt;span&gt;&apos;emailAddress&apos;&lt;/span&gt;&lt;span&gt;] }] });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// an association pointing at a key that will not exist&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;User.&lt;/span&gt;&lt;span&gt;belongsTo&lt;/span&gt;&lt;span&gt;(Company, { foreignKey: &lt;/span&gt;&lt;span&gt;&apos;employerId&apos;&lt;/span&gt;&lt;span&gt;, as: &lt;/span&gt;&lt;span&gt;&apos;employer&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// queries asking for an attribute and a relation that will not exist&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; User.&lt;/span&gt;&lt;span&gt;findAll&lt;/span&gt;&lt;span&gt;({ attributes: [&lt;/span&gt;&lt;span&gt;&apos;id&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;emailAddress&apos;&lt;/span&gt;&lt;span&gt;] });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; User.&lt;/span&gt;&lt;span&gt;findAll&lt;/span&gt;&lt;span&gt;({ include: [&lt;/span&gt;&lt;span&gt;&apos;employer&apos;&lt;/span&gt;&lt;span&gt;] });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;All four compile after the rename. So do 12 of the 19 probes, and
nothing warns you about any of them.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;typeorm-decorators-fine-strings-not&quot;&gt;TypeORM: decorators fine, strings not&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;In TypeORM, the decorators aren’t the problem. &lt;code dir=&quot;auto&quot;&gt;@Index((user: User) =&gt; [user.employerId])&lt;/code&gt; takes a callback, so F2 edits it and a stale name is a type error. That part works.&lt;/p&gt;
&lt;p&gt;The SQL inside those same decorators doesn’t:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;TypeORM&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Index&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; [user.employerId], { where: &lt;/span&gt;&lt;span&gt;`&quot;emailAddress&quot; &amp;#x3C;&gt; &apos;&apos;`&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Check&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&quot;emailAddress&quot; &amp;#x3C;&gt; &apos;&apos;`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Column&lt;/span&gt;&lt;span&gt;({ generatedType: &lt;/span&gt;&lt;span&gt;&apos;STORED&apos;&lt;/span&gt;&lt;span&gt;, asExpression: &lt;/span&gt;&lt;span&gt;&apos;lower(&quot;emailAddress&quot;)&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The callback half renames and the string half doesn’t. Three of TypeORM’s
5 silent misses are that pattern; the other two are an inverse relation and a query.&lt;/p&gt;
&lt;p&gt;The whole table comes down to one question: whether the name goes through something the compiler resolves.
&lt;code dir=&quot;auto&quot;&gt;mappedBy((user) =&gt; user.employer)&lt;/code&gt; is a property read. &lt;code dir=&quot;auto&quot;&gt;as: &apos;employer&apos;&lt;/code&gt; is a string that looks like one.
Drizzle sits high because its schema is values its queries import, so a stale name either follows or fails
to compile. UQL follows all 19 because it never took a string option anywhere, indexes, checks
and raw SQL included.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The files, the renames and every verdict are
&lt;a href=&quot;https://github.com/rogerpadilla/ts-orm-benchmark#rename-safety&quot;&gt;ts-orm-benchmark&lt;/a&gt;’s, on each ORM’s current
stable release and its most type-safe entity API. Every rename is TypeScript 7.0.2’s
own, or prisma-language-server 31.12.15’s for &lt;code dir=&quot;auto&quot;&gt;schema.prisma&lt;/code&gt;. I wrote UQL, so weigh that last
row accordingly and then go check it: &lt;a href=&quot;https://uql-orm.dev/rename-safety&quot;&gt;the rename page&lt;/a&gt; runs the real compiler in your
browser instead of showing you my screenshot of it. Press F2 yourself.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>types</category><category>typescript</category><category>orm</category></item><item><title>In search of the type-safest ORM</title><link>https://uql-orm.dev/blog/in-search-of-the-type-safest-orm</link><guid isPermaLink="true">https://uql-orm.dev/blog/in-search-of-the-type-safest-orm</guid><description>I resisted narrowing find results, because a narrowed row is no longer the entity. Then I upgraded every project I maintain and none of them needed the entity back.</description><pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;a href=&quot;https://github.com/rogerpadilla/ts-orm-benchmark#type-safety&quot;&gt;type-safety benchmark&lt;/a&gt; writes ten ordinary mistakes in six ORMs’ own APIs and compiles them, so “type-safe” stops being a word on a homepage and becomes a count. UQL caught nine. This is the story of the tenth, and of why I refused to fix it for a while.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-mistake&quot;&gt;The mistake&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;(User, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, name: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;user.email; &lt;/span&gt;&lt;span&gt;// undefined at runtime. Every time.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;email&lt;/code&gt; column was never selected, so the property is not there. Nothing objected, because the result was typed as the whole entity no matter what the query asked for. The &lt;code dir=&quot;auto&quot;&gt;undefined&lt;/code&gt; travels into a response body or the next update, and surfaces days later as a null column nobody wrote.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-i-left-it-alone-temporarily&quot;&gt;Why I left it alone (temporarily)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I knew how to fix it. At first I didn’t want to, and the reason wasn’t the type machinery.&lt;/p&gt;
&lt;p&gt;Narrowing takes something away. Once a row is &lt;code dir=&quot;auto&quot;&gt;{ id, name }&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;User&lt;/code&gt;, it stops being the loose bag application code likes to pass around: hand it to a helper typed &lt;code dir=&quot;auto&quot;&gt;(user: User) =&gt; ...&lt;/code&gt; and it’s rejected. I wasn’t keen to trade a daily convenience for a compile error on a mistake I rarely make.&lt;/p&gt;
&lt;p&gt;So I built it, upgraded every project I maintain, the professional ones included, and counted the damage. There was none: &lt;a href=&quot;https://variability.ai&quot;&gt;Variability&lt;/a&gt; alone has 200+ find calls across 25+ files, and it compiled clean, unchanged.&lt;/p&gt;
&lt;p&gt;The pattern I was protecting turned out to be one I don’t actually write. Code that tops up a row starts from a full entity, because it needs the fields; code that projects ships the row straight out, to an API response, a list, a picker.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-uql-still-catches-the-typo&quot;&gt;Why UQL still catches the typo&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;To shape the row, the query has to be captured as a type parameter, and that is where TypeScript changed. TypeScript 6.0 stopped reporting unknown keys on a literal checked against a captured map. Prisma’s and Drizzle’s projections are exactly that shape, which is why a misspelled column &lt;a href=&quot;https://uql-orm.dev/type-safety&quot;&gt;compiles for both today&lt;/a&gt;, and why they sit at nine and eight rather than ten.&lt;/p&gt;
&lt;p&gt;UQL captures the field &lt;em&gt;names&lt;/em&gt; instead of the map that holds them, so the compiler is asked a different question:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$select&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;emial&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// against a captured map:  is &apos;emial&apos; a known key?   - no longer reported&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// against captured names:  &apos;id&apos; | &apos;emial&apos; extends FieldKey&amp;#x3C;User&gt;?   - &apos;emial&apos; is not a field&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The second is a constraint, and constraints never went anywhere. &lt;code dir=&quot;auto&quot;&gt;$where&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;$sort&lt;/code&gt; and each populated relation aren’t captured at all, so they keep the checks they always had.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-does-it-look-like-now&quot;&gt;What does it look like now?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The row is what you asked for, and the compiler knows it:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;(User, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$select: { name: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$populate: { posts: { $select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, title: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; } } },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;user.name; &lt;/span&gt;&lt;span&gt;// string&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;user.posts.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; post.title); &lt;/span&gt;&lt;span&gt;// a list, empty at worst, no guard needed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;user.email; &lt;/span&gt;&lt;span&gt;// compile error: not selected&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;user.profile; &lt;/span&gt;&lt;span&gt;// compile error: not populated&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A query with no projection still returns the whole entity, so nothing changes where you didn’t ask for anything. And where a helper genuinely has to take a projected row, name the shape rather than widening the query:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; { QueryFindResult } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;uql-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserCard&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;QueryFindResult&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;id&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;name&apos;&lt;/span&gt;&lt;span&gt;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That is ten of ten on the &lt;a href=&quot;https://uql-orm.dev/type-safety&quot;&gt;benchmark&lt;/a&gt;, the only entry with no red mark.&lt;/p&gt;

&lt;figure&gt;&lt;video src=&quot;/type-safety.mp4&quot; poster=&quot;/type-safety-poster.webp&quot; width=&quot;720&quot; height=&quot;760&quot; preload=&quot;metadata&quot; autoplay muted loop playsinline controls aria-label=&quot;The type-safety demo cycling through Drizzle, MikroORM, Prisma, Sequelize, TypeORM and UQL, each file&amp;#x27;s mistakes underlined live by the compiler&quot;&gt;&lt;/video&gt;&lt;figcaption&gt;Each ORM&apos;s ten mistakes, underlined by the compiler as the tab changes. &lt;a href=&quot;https://uql-orm.dev/type-safety&quot;&gt;Type it yourself&lt;/a&gt;.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Ten probes can’t separate six ORMs the way a microsecond can, and I chose the ten. So the claim is the narrow one: these are ten mistakes people make on an ordinary afternoon, and this is the ORM that refuses all of them. Every probe is in &lt;a href=&quot;https://github.com/rogerpadilla/ts-orm-benchmark/tree/main/type-safety&quot;&gt;type-safety/&lt;/a&gt;, compiled as written and again with each mistake corrected. Clone it and check.&lt;/p&gt;
&lt;p&gt;And if you’re where I was, weighing a check you suspect you won’t need against a convenience you’re sure you use: upgrade a branch and count the damage. It’s a cheaper argument than the one I had with myself.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>typescript</category><category>types</category><category>orm</category></item><item><title>Semantic search: native vector similarity in a multi-dialect ORM</title><link>https://uql-orm.dev/blog/semantic-search</link><guid isPermaLink="true">https://uql-orm.dev/blog/semantic-search</guid><description>UQL 0.3 ships native semantic search across PostgreSQL, MariaDB, SQLite, and MongoDB Atlas, with automatic index migration.</description><pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most ORMs stop short of vector similarity. The moment you need it, you drop to raw SQL, hand-writing distance expressions and working around dialect quirks outside your type-safe query API.&lt;/p&gt;
&lt;p&gt;UQL 0.3 adds native semantic search to the regular query API, including automatic index migration for HNSW and IVFFlat indexes.&lt;/p&gt;
&lt;aside aria-label=&quot;Updated September 2026&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Updated September 2026&lt;/p&gt;&lt;div&gt;&lt;p&gt;Vector search has since reached CockroachDB, libSQL, Turso and SQL Server 2025, and gained &lt;a href=&quot;https://uql-orm.dev/querying/semantic-search#distance-predicate&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;$near&lt;/code&gt;&lt;/a&gt; to filter by distance and &lt;a href=&quot;https://uql-orm.dev/querying/semantic-search#tuning-recall&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;$candidates&lt;/code&gt;&lt;/a&gt; to tune recall. The &lt;a href=&quot;https://uql-orm.dev/querying/semantic-search&quot;&gt;reference&lt;/a&gt; has the current per-engine detail.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-looks-like&quot;&gt;What it looks like&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;You write&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;queryVec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;embed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;How do vector indexes work?&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;results&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;(Article, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, title: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$sort: { embedding: { $vector: queryVec, $distance: &lt;/span&gt;&lt;span&gt;&apos;cosine&apos;&lt;/span&gt;&lt;span&gt; } },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$limit: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;UQL generates the right SQL for your database:&lt;/p&gt;
&lt;div&gt;&lt;starlight-tabs data-sync-key=&quot;dialect&quot;&gt;&lt;div&gt;&lt;ul role=&quot;tablist&quot;&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-0-0&quot; id=&quot;tab-0-0&quot; aria-selected=&quot;true&quot; tabindex=&quot;0&quot;&gt;PostgreSQL&lt;/a&gt;&lt;/li&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-0-1&quot; id=&quot;tab-0-1&quot; aria-selected=&quot;false&quot; tabindex=&quot;-1&quot;&gt;MariaDB&lt;/a&gt;&lt;/li&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-0-2&quot; id=&quot;tab-0-2&quot; aria-selected=&quot;false&quot; tabindex=&quot;-1&quot;&gt;SQLite&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-0-0&quot; aria-labelledby=&quot;tab-0-0&quot; role=&quot;tabpanel&quot;&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Article&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;embedding&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;=&gt;&lt;/span&gt;&lt;span&gt; $&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;vector&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;LIMIT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-0-1&quot; aria-labelledby=&quot;tab-0-1&quot; role=&quot;tabpanel&quot; hidden&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`id`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`title`&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`Article`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; VEC_DISTANCE_COSINE(&lt;/span&gt;&lt;span&gt;`embedding`&lt;/span&gt;&lt;span&gt;, VEC_FromText(?))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;LIMIT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-0-2&quot; aria-labelledby=&quot;tab-0-2&quot; role=&quot;tabpanel&quot; hidden&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`id`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`title`&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`Article`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; vec_distance_cosine(&lt;/span&gt;&lt;span&gt;`embedding`&lt;/span&gt;&lt;span&gt;, ?)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;LIMIT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;starlight-tabs-restore&gt;&lt;/starlight-tabs-restore&gt;&lt;/starlight-tabs&gt;&lt;/div&gt;
&lt;p&gt;The same query works on every dialect, with no raw SQL or dialect checks in your code.&lt;/p&gt;
&lt;p&gt;For MongoDB, UQL translates the same query into an Atlas &lt;code dir=&quot;auto&quot;&gt;$vectorSearch&lt;/code&gt; pipeline. The Atlas search index itself has to be created in Atlas; see the &lt;a href=&quot;https://uql-orm.dev/querying/semantic-search&quot;&gt;reference&lt;/a&gt; for details.&lt;/p&gt;
&lt;div&gt;&lt;starlight-tabs data-sync-key=&quot;dialect&quot;&gt;&lt;div&gt;&lt;ul role=&quot;tablist&quot;&gt;&lt;li role=&quot;presentation&quot;&gt;&lt;a role=&quot;tab&quot; href=&quot;#tab-panel-1-0&quot; id=&quot;tab-1-0&quot; aria-selected=&quot;true&quot; tabindex=&quot;0&quot;&gt;MongoDB Atlas&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div id=&quot;tab-panel-1-0&quot; aria-labelledby=&quot;tab-1-0&quot; role=&quot;tabpanel&quot;&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;$vectorSearch&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;index&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;embedding_index&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;path&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;embedding&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;queryVector&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;...&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;numCandidates&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;limit&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;starlight-tabs-restore&gt;&lt;/starlight-tabs-restore&gt;&lt;/starlight-tabs&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;entity-setup&quot;&gt;Entity setup&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Define your vector field and index; UQL handles schema generation, extension creation, and index building:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Entity, Id, Field, Index } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;uql-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Entity&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Index&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;article&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; [article.embedding], {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;type: &lt;/span&gt;&lt;span&gt;&apos;hnsw&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;distance: &lt;/span&gt;&lt;span&gt;&apos;cosine&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;m: &lt;/span&gt;&lt;span&gt;16&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;efConstruction: &lt;/span&gt;&lt;span&gt;64&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Article&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;span&gt;({ type: Number }) &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: String }) &lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: &lt;/span&gt;&lt;span&gt;&apos;vector&apos;&lt;/span&gt;&lt;span&gt;, dimensions: &lt;/span&gt;&lt;span&gt;1536&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;embedding&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For Postgres, UQL automatically emits &lt;code dir=&quot;auto&quot;&gt;CREATE EXTENSION IF NOT EXISTS vector&lt;/code&gt;. MariaDB has vector support built in; SQLite requires loading the &lt;a href=&quot;https://github.com/asg017/sqlite-vec&quot;&gt;sqlite-vec&lt;/a&gt; extension.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-else-shipped&quot;&gt;What else shipped&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;$project&lt;/code&gt; returns the computed distance as a named field, typed with the exported &lt;code dir=&quot;auto&quot;&gt;WithDistance&lt;/code&gt; helper, without computing it twice. Alongside it came four distance metrics (&lt;code dir=&quot;auto&quot;&gt;cosine&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;l2&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;inner&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;l1&lt;/code&gt;), three vector types (&lt;code dir=&quot;auto&quot;&gt;vector&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;halfvec&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;sparsevec&lt;/code&gt;), and HNSW, IVFFlat and native vector indexes, each where the engine has it. Which engine has which lives in the &lt;a href=&quot;https://uql-orm.dev/querying/semantic-search&quot;&gt;reference&lt;/a&gt;, kept current as engines are added.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-sort&quot;&gt;Why &lt;code dir=&quot;auto&quot;&gt;$sort&lt;/code&gt;?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Vector similarity search is fundamentally sorting by distance. UQL reuses the existing &lt;code dir=&quot;auto&quot;&gt;$sort&lt;/code&gt; API, which combines with &lt;code dir=&quot;auto&quot;&gt;$where&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;$select&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;$limit&lt;/code&gt;, and regular sort fields:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;results&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;(Article, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$where: { category: &lt;/span&gt;&lt;span&gt;&apos;science&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$sort: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;embedding: { $vector: queryVec, $distance: &lt;/span&gt;&lt;span&gt;&apos;cosine&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;title: &lt;/span&gt;&lt;span&gt;&apos;asc&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$limit: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get started&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;uql-orm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/querying/semantic-search&quot;&gt;Full documentation&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/comparison&quot;&gt;Comparison&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rogerpadilla/uql&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you run into issues or missing features, open an issue on &lt;a href=&quot;https://github.com/rogerpadilla/uql&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>orm</category><category>semantic-search</category><category>vector</category></item><item><title>Standard decorators: props &amp; cons</title><link>https://uql-orm.dev/blog/standard-decorators</link><guid isPermaLink="true">https://uql-orm.dev/blog/standard-decorators</guid><description>The annotation is now checked against the property it describes, which legacy decorators could never do. It cost three decorators, parameter injection, and NestJS compatibility.</description><pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;UQL 0.23 moved its decorators to the &lt;a href=&quot;https://github.com/tc39/proposal-decorators&quot;&gt;TC39 standard spec&lt;/a&gt;. &lt;code dir=&quot;auto&quot;&gt;experimentalDecorators&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;emitDecoratorMetadata&lt;/code&gt; are gone, &lt;code dir=&quot;auto&quot;&gt;reflect-metadata&lt;/code&gt; is gone, and one thing became possible that was not possible before.&lt;/p&gt;
&lt;p&gt;It also deleted three decorators, removed parameter injection, and locked NestJS out of the decorator API entirely. This post covers both sides.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-looks-like&quot;&gt;What it looks like&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Before: legacy decorators&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Entity, Field, Id, ManyToOne } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;uql-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;reflect-metadata&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Entity&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ManyToOne&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;company&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Relation&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Company&lt;/span&gt;&lt;span&gt;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;After: the standard spec&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Entity&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;span&gt;({ type: Number })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: String })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ &lt;/span&gt;&lt;span&gt;references&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; Company })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;companyId&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ManyToOne&lt;/span&gt;&lt;span&gt;({ &lt;/span&gt;&lt;span&gt;entity&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; Company, &lt;/span&gt;&lt;span&gt;references&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; user.companyId })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;company&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Company&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It is more typing: &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; on every field and &lt;code dir=&quot;auto&quot;&gt;entity&lt;/code&gt; on every relation, because nothing is reflected any more. The next section shows what the compiler does with it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-declared-type-is-checked-against-the-property&quot;&gt;The declared type is checked against the property&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Under the legacy spec, a property decorator has this shape:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PropertyDecorator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;target&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;propertyKey&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;symbol&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;There is no type parameter carrying the property’s type. The decorator is handed a key and nothing else, so the options you pass it are just data. Write this and it compiled fine:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: String })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;age&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; number;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You got a &lt;code dir=&quot;auto&quot;&gt;TEXT&lt;/code&gt; column for a number, and you found out when the data looked wrong. &lt;code dir=&quot;auto&quot;&gt;emitDecoratorMetadata&lt;/code&gt; did not save you, because an explicit &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; skipped inference entirely:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;packages/uql-orm/src/entity/metadata/definition.ts (0.22)&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (opts.type) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;opts, typeInferred: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;opts, type: &lt;/span&gt;&lt;span&gt;inferType&lt;/span&gt;&lt;span&gt;(entity, key), typeInferred: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Reflection only filled in a missing type; it never checked a stated one against the property.&lt;/p&gt;
&lt;p&gt;The standard spec hands a field decorator a &lt;code dir=&quot;auto&quot;&gt;ClassFieldDecoratorContext&amp;#x3C;This, Value&gt;&lt;/code&gt;, which is generic in the field’s value type. That difference means a decorator can now constrain what it may be attached to:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;packages/uql-orm/src/entity/decorator/members.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;/** A member decorator that also constrains the property it may be applied to. */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MemberDecorator&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;V&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ClassFieldDecoratorContext&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;V&lt;/span&gt;&lt;span&gt;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;/** A declared `type` wins; otherwise the column is the referenced primary key&apos;s own type. */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DeclaredValue&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;infer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FieldType&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TsTypeOf&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;references&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EntityGetter&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;infer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;E&lt;/span&gt;&lt;span&gt;&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IdValue&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;E&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FieldOptions&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;DeclaredValue&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&gt;&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;({ &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FieldType&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;references&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EntityGetter&lt;/span&gt;&lt;span&gt; }),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;opts&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MemberDecorator&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;DeclaredValue&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;@Field({ type: String })&lt;/code&gt; returns a decorator that only applies to &lt;code dir=&quot;auto&quot;&gt;string | undefined&lt;/code&gt;. Put it on a &lt;code dir=&quot;auto&quot;&gt;number&lt;/code&gt; and it does not compile. The mandatory &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; stopped being redundant typing and became a claim the compiler checks.&lt;/p&gt;
&lt;p&gt;Once one option can name the value a property holds, the others have to agree with it:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { OneToMany } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;uql-orm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ManyToOne&lt;/span&gt;&lt;span&gt;({ &lt;/span&gt;&lt;span&gt;entity&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; Company, &lt;/span&gt;&lt;span&gt;references&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; user.companyId })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;company&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; number;         &lt;/span&gt;&lt;span&gt;// error: a Company relation holds a Company, not its key&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;OneToMany&lt;/span&gt;&lt;span&gt;({ &lt;/span&gt;&lt;span&gt;entity&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; User, &lt;/span&gt;&lt;span&gt;mappedBy&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; user.company })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; User;             &lt;/span&gt;&lt;span&gt;// error: a to-many cardinality needs an array&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ type: &lt;/span&gt;&lt;span&gt;&apos;int&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;createdAt&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; Date;         &lt;/span&gt;&lt;span&gt;// error: a Date field is not an integer column&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Field&lt;/span&gt;&lt;span&gt;({ &lt;/span&gt;&lt;span&gt;references&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; User })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;authorId&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; string;        &lt;/span&gt;&lt;span&gt;// error: User&apos;s key is a number, so this column is one too&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;span&gt;({ type: &lt;/span&gt;&lt;span&gt;&apos;uuid&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;onInsert&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;42&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; string;              &lt;/span&gt;&lt;span&gt;// error: a uuid column is not stamped with a number&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;None of these were catchable before. The migration did not introduce these bugs; it made existing ones visible. A type-test suite pins every one of them and fails the build if any stops erroring, and all but the foreign key survive being reached through the imperative &lt;code dir=&quot;auto&quot;&gt;defineEntity&lt;/code&gt; too.&lt;/p&gt;
&lt;p&gt;The last two arrived later, in 0.24.3. Same mechanism, applied to the two other things that decide what a column holds: the key a foreign key points at, and the generator that stamps a value into it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-reflection-was-costing&quot;&gt;What reflection was costing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;reflect-metadata&lt;/code&gt; was 264 KB, carried for one call: &lt;code dir=&quot;auto&quot;&gt;Reflect.getMetadata(&apos;design:type&apos;, ...)&lt;/code&gt;. It also required every consumer to import it once, globally, before any entity loaded, and to remember to keep two compiler flags on.&lt;/p&gt;
&lt;p&gt;It also did less than it seemed. Reflected types could not survive a circular import, which is why &lt;code dir=&quot;auto&quot;&gt;Relation&amp;#x3C;T&gt;&lt;/code&gt; existed at all: an alias whose only job was to break a cycle that reflection itself created. It is deleted now.&lt;/p&gt;
&lt;p&gt;Dropping it is part of why the package &lt;a href=&quot;https://uql-orm.dev/blog/zero-dependencies&quot;&gt;installs 1 MB with no dependencies&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-we-gave-up&quot;&gt;What we gave up&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;@InjectQuerier()&lt;/code&gt; is gone.&lt;/strong&gt; The standard spec has no parameter decorators, and the &lt;a href=&quot;https://github.com/tc39/proposal-class-method-parameter-decorators&quot;&gt;TC39 proposal for them&lt;/a&gt; is still Stage 1. A &lt;code dir=&quot;auto&quot;&gt;@Transactional()&lt;/code&gt; method read its querier from &lt;code dir=&quot;auto&quot;&gt;AsyncLocalStorage&lt;/code&gt; instead, through &lt;code dir=&quot;auto&quot;&gt;currentQuerier()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;@Transactional()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;currentQuerier()&lt;/code&gt; are gone too&lt;/strong&gt;, since 0.29.0. Nothing but the decorator ever published that ambient querier, so &lt;code dir=&quot;auto&quot;&gt;currentQuerier()&lt;/code&gt; threw inside &lt;code dir=&quot;auto&quot;&gt;pool.transaction()&lt;/code&gt; and every other entry point. A transaction is &lt;a href=&quot;https://uql-orm.dev/querying/transactions&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;pool.transaction(async (querier) =&gt; ...)&lt;/code&gt;&lt;/a&gt;, which needs no class, no decorator and no async-local storage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;@Log()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;@Serialized()&lt;/code&gt; are gone.&lt;/strong&gt; A standard-spec decorator cannot preserve a generic method’s signature, so both would have quietly widened the types of anything they wrapped. The useful half of &lt;code dir=&quot;auto&quot;&gt;@Log()&lt;/code&gt;, error enrichment, was moved into the querier itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NestJS projects cannot use UQL’s decorators at all.&lt;/strong&gt; Nest injects constructor parameters with a parameter decorator, so a Nest project keeps &lt;code dir=&quot;auto&quot;&gt;experimentalDecorators: true&lt;/code&gt;, and one &lt;code dir=&quot;auto&quot;&gt;tsconfig.json&lt;/code&gt; cannot mix specs. This is not a temporary gap that closes when a proposal advances a stage. Those projects use &lt;a href=&quot;https://uql-orm.dev/entities/imperative&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;defineEntity&lt;/code&gt;&lt;/a&gt; instead, which carries the same checks, bar the foreign key one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;target: &apos;esnext&apos;&lt;/code&gt; is now forbidden.&lt;/strong&gt; It is the one target where TypeScript emits decorator syntax untransformed, which Node and every browser reject with a &lt;code dir=&quot;auto&quot;&gt;SyntaxError&lt;/code&gt;. Every dated target downlevels it correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;declare&lt;/code&gt; fields stop working.&lt;/strong&gt; The spec has nothing to decorate on a &lt;code dir=&quot;auto&quot;&gt;declare&lt;/code&gt; member, so narrowing an inherited relation needs a real field with an initializer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Oxc, Vite 8’s default transformer, implements no decorators at all.&lt;/strong&gt; It preserves &lt;code dir=&quot;auto&quot;&gt;@Entity()&lt;/code&gt; verbatim at every target, so a Vite 8 project needs esbuild, SWC or Babel through a plugin. esbuild, SWC, Babel with &lt;code dir=&quot;auto&quot;&gt;version: &apos;2023-11&apos;&lt;/code&gt;, Bun and &lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt; all handle the spec.&lt;/p&gt;
&lt;p&gt;Node 24 is the new minimum, shipped in the same release.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-two-things-that-nearly-broke-it&quot;&gt;The two things that nearly broke it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Neither is in the spec documents, and both took time to track down.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Symbol.metadata&lt;/code&gt; does not exist yet.&lt;/strong&gt; No runtime we support defines it, checked on Node 24 and Bun 1.3. TypeScript’s decorator emit reads it to decide whether to build the metadata object at all, so without a polyfill every &lt;code dir=&quot;auto&quot;&gt;context.metadata&lt;/code&gt; is &lt;code dir=&quot;auto&quot;&gt;undefined&lt;/code&gt; and field registration is silently dropped rather than failing. UQL defines it with &lt;code dir=&quot;auto&quot;&gt;Symbol.for&lt;/code&gt;, not &lt;code dir=&quot;auto&quot;&gt;Symbol()&lt;/code&gt;, so a duplicated copy of the module under HMR or dual-loading lands on the same symbol, and so it agrees with the key esbuild and SWC fall back to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tsc and SWC disagree about inheritance.&lt;/strong&gt; tsc chains a subclass’s &lt;code dir=&quot;auto&quot;&gt;context.metadata&lt;/code&gt; to its parent’s. SWC does not, in any decorator version. Anything built on that prototype chain works under one compiler and quietly loses inherited fields under the other. UQL resolves inheritance by walking the class prototype chain instead, and there is a test that constructs the unchained shape SWC emits to prove it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-codemod&quot;&gt;The codemod&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;uql-codemod&lt;/code&gt; does most of the mechanical work, reading types from the real type checker rather than guessing from the parse tree:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;uql-codemod&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--dry-run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;uql-codemod&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It rewrites &lt;code dir=&quot;auto&quot;&gt;@Field&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;@Id&lt;/code&gt; types, relation &lt;code dir=&quot;auto&quot;&gt;entity&lt;/code&gt; getters and &lt;code dir=&quot;auto&quot;&gt;Relation&amp;#x3C;T&gt;&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;T&lt;/code&gt;, and strips both flags from &lt;code dir=&quot;auto&quot;&gt;tsconfig.json&lt;/code&gt; while preserving your comments and formatting. It refuses to touch what it cannot be sure about: &lt;code dir=&quot;auto&quot;&gt;target: esnext&lt;/code&gt;, values inherited through &lt;code dir=&quot;auto&quot;&gt;extends&lt;/code&gt;, every removed decorator, options objects it cannot read, and branded string ids.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt; does the rest of the migration. The annotations the codemod inserts are checked against the properties they describe, so anything it got wrong is a compile error rather than a silently wrong column.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;was-it-worth-it&quot;&gt;Was it worth it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Yes, though not because of the compiler flags.&lt;/p&gt;
&lt;p&gt;Removing two tsconfig settings and a 264 KB polyfill is worth something, but it is housekeeping. The reason to do this was that reflection put the type in two places and let them drift, and no amount of care fixes a design where the compiler cannot see the mistake. Now there is one declaration and the compiler checks it against the property.&lt;/p&gt;
&lt;p&gt;It did cost something: three decorators, parameter injection, and a framework’s worth of users pushed onto a different API. If you are on NestJS, this release cost you something and handed back a smaller install. That is a worse trade than everyone else got, and not one I can improve until TC39 moves.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get started&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;uql-orm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;uql-codemod&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--dry-run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/upgrade-guide&quot;&gt;Upgrade guide&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/entities/basic&quot;&gt;Entities: decorators&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/entities/imperative&quot;&gt;the imperative API&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/blog/zero-dependencies&quot;&gt;Zero dependencies: what we deleted to fit on the edge&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/comparison&quot;&gt;Comparison&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rogerpadilla/uql&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the codemod leaves something behind that it could have handled, open an issue with the entity that tripped it.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>typescript</category><category>decorators</category><category>orm</category></item><item><title>How much memory does an ORM cost per request?</title><link>https://uql-orm.dev/blog/measuring-orm-memory</link><guid isPermaLink="true">https://uql-orm.dev/blog/measuring-orm-memory</guid><description>Three ways I measured per-request allocation wrong before I got one I trusted, and why the result isn&apos;t the leak table it looks like.</description><pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Someone on r/bun &lt;a href=&quot;https://www.reddit.com/r/bun/comments/1w2s434/comment/p6vlefo/&quot;&gt;asked how UQL’s RAM usage compares to Drizzle’s&lt;/a&gt;.
I didn’t have an answer, so I measured it: weigh the heap, run the query, weigh it again, subtract.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;three-wrong-numbers-at-the-beginning&quot;&gt;Three wrong numbers at the beginning&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Bun reported zero.&lt;/strong&gt; JavaScriptCore refreshes &lt;code dir=&quot;auto&quot;&gt;heapUsed&lt;/code&gt; only when it collects, so every entry looked
like it had allocated nothing. The memory run is on Node, minus the two Bun SQL entries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A forced &lt;code dir=&quot;auto&quot;&gt;global.gc()&lt;/code&gt; added 19%.&lt;/strong&gt; It frees compiled code along with the garbage, and the rounds
after it re-optimise as they run, allocating extra.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One process ranked them by start order.&lt;/strong&gt; The first entry paid to JIT what the rest inherited warm.
One process per entry now.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-came-out&quot;&gt;What came out&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Against hand-written &lt;code dir=&quot;auto&quot;&gt;raw pg&lt;/code&gt;, rows mapped by hand, so what you see is the ORM’s own allocation.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;Total KB&lt;/th&gt;
&lt;th&gt;Adds KB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;raw pg&lt;/td&gt;
&lt;td&gt;245&lt;/td&gt;
&lt;td&gt;floor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;446&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+201&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;769&lt;/td&gt;
&lt;td&gt;+524&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;1,053&lt;/td&gt;
&lt;td&gt;+808&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;1,065&lt;/td&gt;
&lt;td&gt;+820&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;1,296&lt;/td&gt;
&lt;td&gt;+1,051&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;3,864&lt;/td&gt;
&lt;td&gt;+3,619&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;18x between lightest and heaviest, where the same lifecycle in time spans 10x. One step causes nearly
all of it: the nested read, the only one that turns two result sets into an object graph, costs UQL
184KB and MikroORM 2,060KB.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-survives&quot;&gt;What survives&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Sixty more lifecycles, collected either side, leave the worst entry 42KB heavier (Sequelize), identity
maps and all. What the table prices is the garbage each request hands the collector, not memory that
stays.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-answer&quot;&gt;The answer&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;UQL adds 201KB per request, Drizzle 524KB, MikroORM 3.6MB. That is allocation, not resident memory, on
one machine with no pooling and no concurrency.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rogerpadilla/ts-orm-benchmark&quot;&gt;Run it yourself&lt;/a&gt;: &lt;code dir=&quot;auto&quot;&gt;bun run bench.memory&lt;/code&gt; rewrites
the table it publishes, so the &lt;a href=&quot;https://uql-orm.dev/benchmark#memory&quot;&gt;current numbers&lt;/a&gt; can never drift from the last run.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>benchmark</category><category>performance</category><category>memory</category><category>orm</category></item><item><title>What does an ORM really cost you?</title><link>https://uql-orm.dev/blog/what-orms-really-cost</link><guid isPermaLink="true">https://uql-orm.dev/blog/what-orms-really-cost</guid><description>Six ORMs, one full PostgreSQL lifecycle, timed per step. Prisma comes fifth of six, and one step is responsible for most of it.</description><pubDate>Sun, 09 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My bench now does real stuff, full PostgreSQL round-trip operations.&lt;/p&gt;
&lt;p&gt;Six ORMs, one real PostgreSQL, and a full lifecycle per pass: insert 10 rows, read 200 back with a filter and a sort, update one, read it again, load 50 parents with their children, delete, read the empty table. Every step timed separately, median of 250 interleaved iterations.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-results&quot;&gt;The results&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;Adds&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;+232µs&lt;/td&gt;
&lt;td&gt;1,621µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;+685µs&lt;/td&gt;
&lt;td&gt;2,074µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;+838µs&lt;/td&gt;
&lt;td&gt;2,227µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;+1,185µs&lt;/td&gt;
&lt;td&gt;2,574µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;+1,345µs&lt;/td&gt;
&lt;td&gt;2,734µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;+2,236µs&lt;/td&gt;
&lt;td&gt;3,625µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;Figures as measured when this was written, on &lt;code dir=&quot;auto&quot;&gt;pg&lt;/code&gt; throughout. The harness is re-run as versions move, so the &lt;a href=&quot;https://uql-orm.dev/benchmark&quot;&gt;current results&lt;/a&gt; will differ.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Adds&lt;/strong&gt; is the number to read. Every entry pays the same PostgreSQL bill, so the totals compress into a 2.2x range and every ORM looks about the same. Subtract the floor, which is hand-written SQL with the rows mapped by hand at 1,389µs, and what is left is the ORM’s own contribution. That spans nearly 9.6x.&lt;/p&gt;
&lt;p&gt;Everything above runs on node-postgres, so the driver is not the variable. Four of the six support nothing else.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;prisma&quot;&gt;Prisma&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Prisma is not slow across the board, and it does not finish last; it was MikroORM at 3,625µs against Prisma’s 2,734µs. Prisma’s nested read, the hardest step in the set, is the second fastest of any ORM here at 440µs. Its update, delete and single-row reads are all mid-field.&lt;/p&gt;
&lt;p&gt;It is one step:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;INSERT 10 rows&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;raw pg (hand-written)&lt;/td&gt;
&lt;td&gt;454µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;488µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;625µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;635µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;643µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;668µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prisma&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,366µs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That one step is 2.0x the next slowest ORM and it is most of why Prisma places fifth. Turning on its query log shows what it sends:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;What Prisma sends for createManyAndReturn&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;public&quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&quot;User&quot;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&quot;email&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&quot;companyId&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; ($&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,$&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,$&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;), ($&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;,$&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;,$&lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;), ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;COMMIT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;So &lt;code dir=&quot;auto&quot;&gt;createManyAndReturn&lt;/code&gt; wraps the batch in an explicit transaction, where the other five send one statement and stop. That is reasonable, but it does not explain the gap: a BEGIN/COMMIT pair on this machine costs 88µs, and the gap to TypeORM, the next slowest here, is 698µs. The transaction is about an eighth of it. The rest is Prisma’s own overhead on the way in and out, which is the part you cannot opt out of.&lt;/p&gt;
&lt;p&gt;Prisma 7 dropped its Rust query engine, and the client runtime is now TypeScript. This is the faster Prisma, not the old one.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-nested-read-is-where-orms-are-actually-decided&quot;&gt;The nested read is where ORMs are actually decided&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;It is the only step that loads a relation, and it has the widest spread of any read:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SELECT 50 parents with their children&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;raw pg (hand-written)&lt;/td&gt;
&lt;td&gt;256µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;346µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;440µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;498µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;535µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;680µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;1,205µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This step matters most, because turning two result sets into an object graph without an N+1 is the main job of an ORM, and here the field spans 3.5x.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;run-it-yourself&quot;&gt;Run it yourself&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clone&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;https://github.com/rogerpadilla/ts-orm-benchmark.git&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ts-orm-benchmark&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;bun&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;postgres:///postgres&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bench&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It creates its own database and rewrites its own result tables, so the published numbers cannot drift from the last run. CI runs the full lifecycle with assertions on every push.&lt;/p&gt;
&lt;p&gt;The latest set always lives on the &lt;a href=&quot;https://uql-orm.dev/benchmark&quot;&gt;benchmark page&lt;/a&gt;, and there is a feature-by-feature &lt;a href=&quot;https://uql-orm.dev/comparison&quot;&gt;comparison&lt;/a&gt; if speed is not your only axis. If your numbers come out different, open an issue.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>benchmark</category><category>performance</category><category>postgresql</category><category>orm</category></item><item><title>Zero dependencies: what we deleted</title><link>https://uql-orm.dev/blog/zero-dependencies</link><guid isPermaLink="true">https://uql-orm.dev/blog/zero-dependencies</guid><description>npm i uql-orm installs one package and nothing else. Getting there meant deleting a 1.8 MB transpiler, a 264 KB metadata polyfill, and the sourcemaps nobody read.</description><pubDate>Sun, 02 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;npm i uql-orm&lt;/code&gt; installs one package. No dependencies, every dialect included.&lt;/p&gt;
&lt;p&gt;A month ago it had four mandatory packages, roughly 4 MBs unpacked; now it is 992 KB unpacked.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-release-four-dependencies-gone&quot;&gt;One release, four dependencies gone&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;0.21.0&lt;/code&gt; did it in one pass: ~4.0 MB down to 992 KB, cold start from 16.9 ms to 8.9 ms. Four packages, one job each, none worth a dependency of its own.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;tslib&lt;/code&gt; existed so TypeScript’s helper functions didn’t repeat across files. &lt;code dir=&quot;auto&quot;&gt;&quot;importHelpers&quot;: false&lt;/code&gt; costs a few hundred bytes per file instead of a dependency edge.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;sqlstring&lt;/code&gt; escaped MySQL and MariaDB literals, badly: a &lt;code dir=&quot;auto&quot;&gt;Uint8Array&lt;/code&gt; came out as &lt;code dir=&quot;auto&quot;&gt;`0` = 255&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;X&apos;ff00&apos;&lt;/code&gt;, a plain object as &lt;code dir=&quot;auto&quot;&gt;&apos;[object Object]&apos;&lt;/code&gt;. It did not crash or warn; the output was just wrong. &lt;code dir=&quot;auto&quot;&gt;Dialect.escape&lt;/code&gt; handles all three SQL dialects itself now, byte-for-byte identical across 29 value shapes, and those two cases throw instead of writing garbage.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;reflect-metadata&lt;/code&gt; (264 KB) earned its keep with one call, &lt;code dir=&quot;auto&quot;&gt;Reflect.getMetadata(&apos;design:type&apos;, ...)&lt;/code&gt;, so &lt;code dir=&quot;auto&quot;&gt;@Field()&lt;/code&gt; could guess a column’s type from the property. Optional as of this release, gone two releases later.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;jiti&lt;/code&gt; (1.8 MB), a full TypeScript transpiler, existed to read one file: &lt;code dir=&quot;auto&quot;&gt;uql.config.ts&lt;/code&gt;. That’s a lot of compiler to hire for one sticky note. Also gone; that config now needs a runtime that already speaks TypeScript (&lt;code dir=&quot;auto&quot;&gt;bun&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;node --import tsx&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Then the stuff that was never code: 369 sourcemaps nobody opened, and a 108 KB CHANGELOG, shipped in every install anyway. &lt;code dir=&quot;auto&quot;&gt;files&lt;/code&gt; in &lt;code dir=&quot;auto&quot;&gt;package.json&lt;/code&gt; is &lt;code dir=&quot;auto&quot;&gt;[&quot;dist&quot;, &quot;README.md&quot;]&lt;/code&gt; now.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;then-reflection-went-too&quot;&gt;Then reflection went too&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Two releases later, &lt;code dir=&quot;auto&quot;&gt;reflect-metadata&lt;/code&gt; stopped being optional and started being pointless. &lt;code dir=&quot;auto&quot;&gt;0.23.0&lt;/code&gt; moved decorators to the &lt;a href=&quot;https://github.com/tc39/proposal-decorators&quot;&gt;TC39 standard spec&lt;/a&gt;: &lt;code dir=&quot;auto&quot;&gt;@Field()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;@Id()&lt;/code&gt; require an explicit &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; now, checked against the property, so there’s nothing left to reflect. The full story, including what it cost, is in &lt;a href=&quot;https://uql-orm.dev/blog/standard-decorators&quot;&gt;Standard decorators: props &amp;#x26; cons&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-one-npm-i-puts-on-disk&quot;&gt;What one &lt;code dir=&quot;auto&quot;&gt;npm i&lt;/code&gt; puts on disk&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Install one package into an empty project and count the bytes yourself:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--omit=dev&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;uql-orm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;node_modules&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{}&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Unpacked bytes on disk, not the 288 kB you download. Tarballs are compressed, but a cold start loads the unpacked files.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Installed&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;uql-orm&lt;/code&gt; 0.24.1&lt;/td&gt;
&lt;td&gt;1.0 MB&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;@mikro-orm/&lt;wbr&gt;postgresql&lt;/code&gt; 7.1.9&lt;/td&gt;
&lt;td&gt;4.7 MB&lt;/td&gt;
&lt;td&gt;1,153&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;drizzle-orm&lt;/code&gt; 0.45.2&lt;/td&gt;
&lt;td&gt;9.9 MB&lt;/td&gt;
&lt;td&gt;2,667&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;sequelize&lt;/code&gt; 6.37.8&lt;/td&gt;
&lt;td&gt;15.0 MB&lt;/td&gt;
&lt;td&gt;2,708&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;typeorm&lt;/code&gt; 1.1.0&lt;/td&gt;
&lt;td&gt;22.5 MB&lt;/td&gt;
&lt;td&gt;3,663&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;@prisma/&lt;wbr&gt;client&lt;/code&gt; 7.9.1&lt;/td&gt;
&lt;td&gt;75.0 MB&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;One caveat about this table: 93% of &lt;code dir=&quot;auto&quot;&gt;@prisma/client&lt;/code&gt; is 70 MB of Rust query compilers cross-compiled to WebAssembly, one per engine it supports. UQL ships every dialect too, so that part isn’t a fair hit on Prisma. Per dialect it’s actually smaller: 4.9 MB for its Postgres compiler alone against 19.9 kB gzipped for UQL’s entire Postgres entry point.&lt;/p&gt;
&lt;p&gt;No row includes a driver. Add &lt;code dir=&quot;auto&quot;&gt;pg&lt;/code&gt; to any of them and they all grow the same amount. This table is the floor.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-small-number-doesnt-cover&quot;&gt;What the small number doesn’t cover&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;UQL is small; your driver may not be. &lt;code dir=&quot;auto&quot;&gt;pg&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;mysql2&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;mariadb&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;mongodb&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;better-sqlite3&lt;/code&gt; all have their own weight, and the last one needs a native build. UQL’s promise is narrower than “small”: it adds nothing on top of whichever one you already picked.&lt;/p&gt;
&lt;p&gt;Every entry point has a gzip budget checked on every build, because 0.13.0 once let &lt;code dir=&quot;auto&quot;&gt;node:async_hooks&lt;/code&gt; leak into browser bundles and had to be pulled, so the budget was added after that.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get started&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;uql-orm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/getting-started&quot;&gt;Quick Start&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/blog/standard-decorators&quot;&gt;Standard decorators: props &amp;#x26; cons&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev/comparison&quot;&gt;Comparison&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rogerpadilla/uql&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your install contains something that shouldn’t be there, open an issue.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>performance</category><category>bundle-size</category><category>edge</category><category>orm</category></item><item><title>In search of the fastest TypeScript ORM</title><link>https://uql-orm.dev/blog/in-search-of-the-fastest-typescript-orm</link><guid isPermaLink="true">https://uql-orm.dev/blog/in-search-of-the-fastest-typescript-orm</guid><description>Pure SQL generation speed across 6 TypeScript ORMs and query builders, measured with an open, reproducible benchmark.</description><pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;aside aria-label=&quot;Superseded&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Superseded&lt;/p&gt;&lt;div&gt;&lt;p&gt;This post measures pure SQL-generation speed, with no database involved. That benchmark has since been retired: it turned out to account for well under 0.5% of a real request, so it stopped being a meaningful way to compare ORMs. The &lt;a href=&quot;https://uql-orm.dev/benchmark&quot;&gt;benchmark page&lt;/a&gt; and &lt;a href=&quot;https://uql-orm.dev/blog/what-orms-really-cost&quot;&gt;what-orms-really-cost&lt;/a&gt; now measure a full PostgreSQL round trip instead, and that is the current, maintained comparison. The numbers and prose below are kept as they were for the record, but no longer track any actively re-run benchmark.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;I kept seeing &lt;em&gt;“just use Drizzle, it’s lightweight”&lt;/em&gt; and &lt;em&gt;“ORMs are slow, use a query builder”&lt;/em&gt; repeated everywhere. So I decided to actually measure it.&lt;/p&gt;
&lt;p&gt;The benchmark measures &lt;strong&gt;pure SQL generation speed&lt;/strong&gt;: no database, no network, no connection pool. That isolates the overhead the ORM adds to every request. 6 entries, 8 query types, 3 runs averaged, on an Apple Silicon M4.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;methodology&quot;&gt;Methodology&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Environment:&lt;/strong&gt; Node.js v24.18.1, Apple M4 Pro, 3 runs averaged.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versions:&lt;/strong&gt; Latest stable of every entry as of August 2026 (TypeORM 1.1.0, MikroORM 7.1.9, Sequelize 6.37.8, Drizzle 0.45.2, Knex 3.3.0, UQL 0.24.0).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fairness:&lt;/strong&gt; Each ORM uses its most idiomatic API (QueryBuilder for TypeORM/MikroORM), which benefits them by skipping entity overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What’s measured:&lt;/strong&gt; Pure SQL string generation, with no database, no I/O, and no connection pool. This isolates ORM overhead only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why no Prisma?&lt;/strong&gt; Prisma’s query compiler is Rust compiled to WebAssembly rather than pure JS/TS, and it exposes no public SQL-compilation API, so it cannot be measured this way.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-results&quot;&gt;The results&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;insert-10-rows-in-batch&quot;&gt;INSERT: 10 rows in batch&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;vs winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;698K&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knex&lt;/td&gt;
&lt;td&gt;463K&lt;/td&gt;
&lt;td&gt;0.66x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;196K&lt;/td&gt;
&lt;td&gt;0.28x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;111K&lt;/td&gt;
&lt;td&gt;0.16x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;42K&lt;/td&gt;
&lt;td&gt;0.06x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;12K&lt;/td&gt;
&lt;td&gt;0.02x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;update-set--where&quot;&gt;UPDATE: SET + WHERE&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;vs winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;2,161K&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knex&lt;/td&gt;
&lt;td&gt;695K&lt;/td&gt;
&lt;td&gt;0.32x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;282K&lt;/td&gt;
&lt;td&gt;0.13x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;236K&lt;/td&gt;
&lt;td&gt;0.11x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;218K&lt;/td&gt;
&lt;td&gt;0.10x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;79K&lt;/td&gt;
&lt;td&gt;0.04x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;upsert-on-conflict-by-id&quot;&gt;UPSERT: ON CONFLICT by id&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;vs winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;691K&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knex&lt;/td&gt;
&lt;td&gt;433K&lt;/td&gt;
&lt;td&gt;0.63x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;327K&lt;/td&gt;
&lt;td&gt;0.47x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;261K&lt;/td&gt;
&lt;td&gt;0.38x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;260K&lt;/td&gt;
&lt;td&gt;0.38x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;36K&lt;/td&gt;
&lt;td&gt;0.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;delete-simple-where&quot;&gt;DELETE: simple WHERE&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;vs winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;3,996K&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;1,361K&lt;/td&gt;
&lt;td&gt;0.34x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knex&lt;/td&gt;
&lt;td&gt;1,084K&lt;/td&gt;
&lt;td&gt;0.27x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;507K&lt;/td&gt;
&lt;td&gt;0.13x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;263K&lt;/td&gt;
&lt;td&gt;0.07x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;207K&lt;/td&gt;
&lt;td&gt;0.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;select-1-field&quot;&gt;SELECT: 1 field&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;vs winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;4,675K&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;3,084K&lt;/td&gt;
&lt;td&gt;0.66x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knex&lt;/td&gt;
&lt;td&gt;1,092K&lt;/td&gt;
&lt;td&gt;0.23x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;591K&lt;/td&gt;
&lt;td&gt;0.13x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;565K&lt;/td&gt;
&lt;td&gt;0.12x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;229K&lt;/td&gt;
&lt;td&gt;0.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;select-where--sort--limit&quot;&gt;SELECT: WHERE + SORT + LIMIT&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;vs winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;1,365K&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knex&lt;/td&gt;
&lt;td&gt;614K&lt;/td&gt;
&lt;td&gt;0.45x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;381K&lt;/td&gt;
&lt;td&gt;0.28x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;276K&lt;/td&gt;
&lt;td&gt;0.20x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;73K&lt;/td&gt;
&lt;td&gt;0.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;59K&lt;/td&gt;
&lt;td&gt;0.04x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;select-complex-or--operators&quot;&gt;SELECT: complex $or + operators&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;vs winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;741K&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knex&lt;/td&gt;
&lt;td&gt;243K&lt;/td&gt;
&lt;td&gt;0.33x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;158K&lt;/td&gt;
&lt;td&gt;0.21x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;150K&lt;/td&gt;
&lt;td&gt;0.20x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;34K&lt;/td&gt;
&lt;td&gt;0.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;28K&lt;/td&gt;
&lt;td&gt;0.04x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;aggregate-group-by--count--having&quot;&gt;AGGREGATE: GROUP BY + COUNT + HAVING&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;vs winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UQL&lt;/td&gt;
&lt;td&gt;1,482K&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;416K&lt;/td&gt;
&lt;td&gt;0.28x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knex&lt;/td&gt;
&lt;td&gt;304K&lt;/td&gt;
&lt;td&gt;0.21x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;277K&lt;/td&gt;
&lt;td&gt;0.19x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle&lt;/td&gt;
&lt;td&gt;74K&lt;/td&gt;
&lt;td&gt;0.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroORM&lt;/td&gt;
&lt;td&gt;69K&lt;/td&gt;
&lt;td&gt;0.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://rogerpadilla.github.io/ts-orm-benchmark/chart.html&quot;&gt;Interactive charts&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;two-things-that-surprised-me&quot;&gt;Two things that surprised me&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. The “lightweight” option is the slowest thing in the benchmark.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Drizzle, marketed as lightweight, is slower than Sequelize (a full ORM from 2014) in every single category. The functional expression-tree approach creates more intermediate objects than Sequelize’s simple string concatenation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. A standalone query builder can’t beat a well-designed ORM.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Knex has zero entity/relation overhead; it’s just a SQL string builder. Yet UQL, a full ORM with entities, relations, and migrations, is faster in all 8 categories, by 1.5x on batch INSERT up to 4.9x on aggregates. The conventional wisdom (“ORMs are slow”) doesn’t hold when the ORM pre-computes its metadata and avoids intermediate allocations.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;how-uql-gets-there&quot;&gt;How UQL gets there&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I got curious why the gap was so large, so I dug into the approach. Most ORMs figure out your schema at query time: &lt;em&gt;“What table does &lt;code dir=&quot;auto&quot;&gt;User&lt;/code&gt; map to? What column is &lt;code dir=&quot;auto&quot;&gt;companyId&lt;/code&gt;? Is it nullable?”&lt;/em&gt; They answer these questions on every single query.&lt;/p&gt;
&lt;p&gt;UQL answers them once at startup. Field-to-column mappings, table names, and relation paths are all pre-computed into lookup tables before the first query runs. At query time, generating SQL is just reading from a cache.&lt;/p&gt;
&lt;p&gt;The other difference is allocation. When TypeORM builds a SELECT, it creates a QueryBuilder, then an expression tree, then walks the tree to produce SQL. UQL pushes SQL fragments directly into a string buffer, creating no intermediate objects and no garbage-collection pressure.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;when-this-matters-in-production&quot;&gt;When this matters in production&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Database latency is 1-50ms and ORM overhead is microseconds, so for a low-traffic app the difference is noise. It starts to matter at scale. Take the WHERE + SORT + LIMIT query at 1,000 req/s: UQL (1,365K ops/s) spends under 1ms of CPU per second generating SQL, while MikroORM (73K ops/s) spends roughly 14ms, about 19x more CPU for the same queries before a single byte hits the network. In serverless, where you pay per ms of CPU, that shows up on the bill; in containers, it shows up as horizontal scaling cost.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;reproduce-it&quot;&gt;Reproduce it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Full disclosure: I’m the author of UQL. That’s exactly why I built the benchmark as an independent repo anyone can audit and reproduce.&lt;/p&gt;
&lt;p&gt;This SQL-generation benchmark itself is no longer maintained (see the note above); for the current, actively re-run benchmark and its repro steps, see the &lt;a href=&quot;https://uql-orm.dev/benchmark&quot;&gt;benchmark page&lt;/a&gt;. There’s also a broader feature-by-feature comparison on the &lt;a href=&quot;https://uql-orm.dev/comparison&quot;&gt;comparison page&lt;/a&gt;.&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>benchmark</category><category>performance</category><category>typescript</category><category>orm</category></item><item><title>In search of the perfect TypeScript ORM</title><link>https://uql-orm.dev/blog/in-search-of-the-perfect-orm</link><guid isPermaLink="true">https://uql-orm.dev/blog/in-search-of-the-perfect-orm</guid><description>What makes a perfect ORM? I evaluated the top 5 features every TypeScript ORM should have, and why the existing ones fall short.</description><pubDate>Sat, 15 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href=&quot;https://medium.com/@rogerpadillac/in-search-of-the-perfect-orm-e01fcc9bce3d&quot;&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-an-orm&quot;&gt;What is an ORM?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;An ORM provides a simpler way to interact with databases in an app: it lets developers work with data as objects.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-a-perfect-orm-for-typescript&quot;&gt;What is a perfect ORM for TypeScript?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Below are the ideal features a TypeScript ORM should have, why such features are the most important ones, and how the existing ORMs fall short in most of these areas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The top 5 features a perfect TypeScript ORM should have are:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;1-serializable-queries&quot;&gt;1. Serializable queries&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Queries that can travel between the layers of a system keep the design simple. If the client can send a query to the server over HTTP or websockets using the ORM’s own syntax, you no longer need an extra query language on top: the &lt;code dir=&quot;auto&quot;&gt;GraphQL =&gt; ORM =&gt; Database&lt;/code&gt; flow collapses to &lt;code dir=&quot;auto&quot;&gt;ORM =&gt; Database&lt;/code&gt;. Concretely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No pseudo-language for queries and no context switching: the syntax is standard JSON, entirely declarative and serializable.&lt;/li&gt;
&lt;li&gt;No additional servers and no extra steps in the build process.&lt;/li&gt;
&lt;li&gt;Editors and IDEs understand the queries natively, without custom plugins or extensions.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;2-native-typescript&quot;&gt;2. Native TypeScript&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Use TypeScript itself for everything: JSON queries, classes, and decorators.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Type-safe queries and models that are natively validated by the same language that you use to write your app.&lt;/li&gt;
&lt;li&gt;Context-aware queries allow auto-completion of the appropriate operators and fields according to the different parts of a query.&lt;/li&gt;
&lt;li&gt;Entity definition with standard classes and decorators to avoid the need for proprietary DSLs (as happens with Prisma), extra steps in the build process, and custom extensions for the editors.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;3-multi-level-operators&quot;&gt;3. Multi-level operators&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Operations such as filter, sort, limit, project, and others work on any level of the queries (including relations and their fields).&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;4-consistent-api-across-databases&quot;&gt;4. Consistent API across databases&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Write the queries for any database in a consistent way and then transparently optimize these queries for the configured database dialect.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;5-universal-syntax-language-agnostic&quot;&gt;5. Universal syntax (language agnostic)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Because the queries are standard JSON, they can be produced or consumed from any language: JSON is a first-class format in Python, Rust, Go, and virtually everything else. That also makes UQL implementations in other languages feasible.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-do-the-current-top-3-typescript-orms-fall-short&quot;&gt;Why do the current top 3 TypeScript ORMs fall short?&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;what-does-typeorm-lack-to-be-a-perfect-orm&quot;&gt;What does TypeORM lack to be a perfect ORM?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. Lack of 100% serializable queries:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Notice how the &lt;code dir=&quot;auto&quot;&gt;LessThan&lt;/code&gt; operator is a function that has to be imported and called; that alone makes the query impossible to serialize.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;TypeORM&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { LessThan } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;typeorm&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;loadedPosts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; dataSource.&lt;/span&gt;&lt;span&gt;getRepository&lt;/span&gt;&lt;span&gt;(Post).&lt;/span&gt;&lt;span&gt;findBy&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;likes: &lt;/span&gt;&lt;span&gt;LessThan&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. Lack of Native TypeScript:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The query dissolves into strings because &lt;code dir=&quot;auto&quot;&gt;relations&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;where&lt;/code&gt; accept any string, so any invalid string can go there.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;TypeORM&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;posts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; connection.manager.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(Post, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;select: [&lt;/span&gt;&lt;span&gt;&apos;id&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;relations: [&lt;/span&gt;&lt;span&gt;&apos;&amp;#x3C; anything can go here &gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3. Lack of consistent API across databases:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In the section that TypeORM has for MongoDB, there is a self-explanatory warning about this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Lack of Universal Syntax (language agnostic):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It relies on closures to support advanced queries (and not every language out there supports closures).&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h3 id=&quot;what-does-prisma-lack-to-be-a-perfect-orm&quot;&gt;What does Prisma lack to be a perfect ORM?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. Lack of Native TypeScript:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context-switching between the custom DSL and TypeScript makes this process obtrusive.&lt;/li&gt;
&lt;li&gt;Extra steps in the build process to generate the corresponding files from the custom DSL.&lt;/li&gt;
&lt;li&gt;It is required to install a custom extension for VS Code to get (basic) autocompletion from the editor for the DSL, which is far from being as good and reliable as with TypeScript.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;datasource&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;provider &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;postgresql&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;url      &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;DATABASE_URL&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;generator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;provider &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;prisma-client-js&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id        &lt;/span&gt;&lt;span&gt;Int&lt;/span&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;@id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;autoincrement&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;createdAt &lt;/span&gt;&lt;span&gt;DateTime&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email     &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;@unique&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name      &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role      &lt;/span&gt;&lt;span&gt;Role&lt;/span&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;@default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;USER&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;posts     &lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. Lack of consistent API across databases:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Prisma exposes low-level details about MongoDB that could be encapsulated by the ORM.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt;()) &lt;/span&gt;&lt;span&gt;@map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;_id&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;@db.ObjectId&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Other fields&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3. Lack of Universal Syntax (language agnostic):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It relies on its own proprietary DSL to define the models.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h3 id=&quot;what-does-mikroorm-lack-to-be-a-perfect-orm&quot;&gt;What does MikroORM lack to be a perfect ORM?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. Lack of 100% serializable queries:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Notice how that query uses two separate methods, one for the update and another for the where; that structure cannot be serialized.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;MikroORM&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;qb&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; orm.em.&lt;/span&gt;&lt;span&gt;createQueryBuilder&lt;/span&gt;&lt;span&gt;(Author);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;qb.&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;({ name: &lt;/span&gt;&lt;span&gt;&apos;test 123&apos;&lt;/span&gt;&lt;span&gt;, type: PublisherType.&lt;/span&gt;&lt;span&gt;GLOBAL&lt;/span&gt;&lt;span&gt; }).&lt;/span&gt;&lt;span&gt;where&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id: &lt;/span&gt;&lt;span&gt;123&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;type: PublisherType.&lt;/span&gt;&lt;span&gt;LOCAL&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. Lack of Native TypeScript:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Dissolves into strings: any string can go inside the &lt;code dir=&quot;auto&quot;&gt;fields&lt;/code&gt; array, so the query loses the possibility of being type-safe.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;MikroORM&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; em.&lt;/span&gt;&lt;span&gt;findOne&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Author,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fields: [&lt;/span&gt;&lt;span&gt;&apos;name&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;books.title&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;books.author&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;books.price&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3. Lack of Multi-level operators:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What if you need to filter the records of a relation or sort them? This seems unachievable in a type-safe way from what can be seen in the MikroORM docs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Lack of consistent API across databases:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;MikroORM&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { EntityManager } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;@mikro-orm/mongodb&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; orm.em &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EntityManager&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;qb&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; em.&lt;/span&gt;&lt;span&gt;aggregate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/* ... */&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;MikroORM exposes low-level details about MongoDB that could be encapsulated by the ORM.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-is-uql-the-closest-to-a-perfect-orm&quot;&gt;Why is UQL the closest to a perfect ORM?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;In short, because it was designed from the beginning with all of the above foundations. Let’s see how.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;1-100-serializable-queries&quot;&gt;1. 100% serializable queries&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Even the insert and update operations have a fully serializable API.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lastUsers&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;(User, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, name: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, email: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$sort: { createdAt: &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$limit: &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;2-native-typescript-with-truly-type-safe-queries&quot;&gt;2. Native TypeScript with truly type-safe queries&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Every operator and field is validated according to the context. For example, the possible values for the &lt;code dir=&quot;auto&quot;&gt;$select&lt;/code&gt; operator will automatically depend on the level.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lastUsersWithProfiles&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;(User, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, name: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$populate: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;profile: { $select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, picture: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; }, $required: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$sort: { createdAt: &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$limit: &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;3-multi-level-operators-1&quot;&gt;3. Multi-level operators&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The operators work on any level. For example, &lt;code dir=&quot;auto&quot;&gt;$sort&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;$where&lt;/code&gt; can be applied to the relations and their fields in a type-safe and context-aware way.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;items&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; pool.&lt;/span&gt;&lt;span&gt;findMany&lt;/span&gt;&lt;span&gt;(Item, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, name: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$populate: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;measureUnit: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, name: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$where: { name: { $ne: &lt;/span&gt;&lt;span&gt;&apos;unidad&apos;&lt;/span&gt;&lt;span&gt; } },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$required: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tax: { $select: { id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, name: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; } },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$where: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;price: { $gte: &lt;/span&gt;&lt;span&gt;1000&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: { $istartsWith: &lt;/span&gt;&lt;span&gt;&apos;A&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$sort: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tax: { name: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;measureUnit: { name: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;createdAt: &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$limit: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;4-consistent-api-across-databases-1&quot;&gt;4. Consistent API across databases&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;One API for every database: the same entities and queries transparently work on any supported database, with the dialect-specific SQL (or MongoDB commands) generated for you. This makes it easier to switch from a Document to a Relational database (or vice-versa); the &lt;a href=&quot;https://uql-orm.dev/switching-to-uql&quot;&gt;migration guide&lt;/a&gt; walks through what that move looks like coming from Mongoose.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;5-universal-syntax-across-languages&quot;&gt;5. Universal syntax across languages&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Its syntax is standard JSON, so queries can be produced or consumed from other languages, enabling interoperation or even implementations of UQL in languages such as Python or Rust.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;See more at &lt;strong&gt;&lt;a href=&quot;https://uql-orm.dev&quot;&gt;uql-orm.dev&lt;/a&gt;&lt;/strong&gt;. Please star it ⭐ on &lt;strong&gt;&lt;a href=&quot;https://github.com/rogerpadilla/uql&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt; if you like the idea!&lt;/p&gt;

&lt;hr&gt;&lt;section id=&quot;giscus-wrapper&quot;&gt;&lt;!-- Content injected via client-side script to support View Transitions --&gt;&lt;/section&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://uql-orm.dev/&quot;&gt;UQL&lt;/a&gt; is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>orm</category><category>typescript</category><category>architecture</category></item></channel></rss>