Codemod
uql-codemod rewrites your code across UQL’s mechanical breaking changes and reports what it cannot decide. Run it
first when you upgrade, then tsc, then the upgrade guide for the rest.
npx uql-codemod --dry-runneeds a decision: src/entity/audit.ts:9: has no 'type': declare its column as a builder, such as `f.string()`would change: src/entity/user.ts...23 file(s) would change1 item(s) need a decision; see aboveneeds a decision: marks something it will not guess, and worth a look: a rewrite you may want to read. Drop
--dry-run to write the files. Only the edited text changes; comments and formatting stay as you wrote them.
--project=<path> picks another tsconfig.json (it must cover your entities), and --include=<a,b> limits the
paths. It exits 0 when nothing is left, 1 when something is, and 2 when it could not start.
Everything it inserts is type-checked against the property it describes, so a wrong guess shows up as a tsc error.
What it rewrites
Section titled “What it rewrites”Entities and relations:
- A column’s options to its builder:
@Id()and@Field({ type: String, nullable: true, length: 80 })to@Field(f.number().autoIncrement())and@Field(f.string().nullable().length(80)), a NOT NULL column losing itsnullable: false, and acomputedfield to@Computed((row) => f.string().as(...)), or@Computed((row) => row.books.count())for an aggregate. - A sole numeric key to the generated one it was,
f.number().id()tof.number().autoIncrement(), and.autoIncrement(false)dropped. A key a relation shares stays.id(), as does a class’sid!, which an insert then names until it isid?or listed in[filledKey]. On MongoDB, take.autoIncrement()off a numeric key. - Each index, check and trigger entry to the helper its callback is handed:
indexes: (post, index) => [index([post.title], 'by_title')]. @Index,@Triggerand@Filterinto@Entity’sindexes,triggersandfilters, each decorator’s comments kept above its entry; a class with no@Entitybecomes@Entity({ abstract: true, ... }), a base that is never a table.- Members named by string to callbacks, in
mappedBy,include,references, and theindexesandhooksof@Entity/defineEntity:mappedBy: 'author'to(post) => post.author. references: (post) => post.authorIdon each to-one, declaring theauthorIdcolumn where it is missing.- A JSON index entry to the path read off its column’s ref:
{ column: post.kind, jsonPath: { path: 'theme', type: Number } }to{ column: post.kind.theme, type: Number }, and ajsonArray’s element type totype: [String]. ItslengthbecomescharLength, and a column entry’sprefixLength. | nullon each nullable column’s property, except keys, relation aggregates andnullable: falsefields.- The
idKeybrand on a key not calledid,_idoruuid, and on every composite. virtualtocomputed, and a check’sexpressionand a filter’sconditiontowhere.- A class registered with
defineEntity(Post, { ... })to thedefineEntitycall it stands for,export const Post = defineEntity({ name: 'Post', ... }), the class removed: its other options as the sections chained after it, each relation as arelthunk, a relation aggregate in.relations()’ second callback, a hook’s method as a function of the row, and the same class’sdefineField,defineId,defineRelation,defineHook,defineIndex,defineTriggeranddefineFilterfolded in. A column’s foreign key becomes the relation joining on it, itsonDeletemoved into that relation, and a key no relation joins on gets amanyToOnewritten for it. A class with a method no hook runs, a base it extends, or a foreign key no relation can take is reported and left as written.
SQL and queries:
rawtosql, at the import and every use.raw('SQL')becomes the tagged template,raw(fn, alias)becomessql(fn).as(alias), andcol('cost')becomes a ref:(product) => sql`${product.cost}`in acomputed,refs(Item).costin a statement.- A migration builder column’s options to the same methods:
table.string('email', { length: 80, nullable: true })totable.string('email').length(80).nullable(),.defaultValue()to.default(),.primaryKey()to.id(),.computed()to.as(), and.notNullable()dropped. - A partial-index
wherestring tosql, in entities and the migration builder, and the builder’sexprdefaults:expr.now()tocurrentTimestamp,expr.uuid()touuid,expr.raw('...')tosql`...`. $excludeto a$selectleaving its fields out,$select: { password: false }, at every depth of$populate.- An aggregate’s
$aggto$select, and fields named in it or in$text’s$fieldsto keys:{ $sum: { amount: true } }. - An upsert’s conflict keys to a query,
upsertOne(User, { email: true }, row)toupsertOne(User, { $conflict: { email: true } }, row), and its update, a fourth argument or{ update: X }options, to the query’s$update: X, the options after it kept, inupsertOne,upsertManyand a trigger’supsertInto;QueryConflictPathstoQueryConflictKeys, andconst { id } = await upsertOne(...)toconst id, since it resolves to the id. $lock: { wait: 'skip' }to{ $wait: 'skip' }, ortruefor a lock that waits.runandallto tags:all('SELECT * FROM t WHERE id = ?', [id])toall`SELECT * FROM t WHERE id = ${id}`. A string built at run time goes insql.text(...).
Imports and setup:
- Renamed exports, with their imports and uses:
QueryWhereMap,RelationKeyMap,FilterCondition,IndexColumnSchema(toIndexEntry),IndexColumnModifiers(toIndexEntryModifiers),QueryDialect,EngineFeatures,KnownMigratorDialect(toDialectName),D1PreparerandD1Database(toD1Queryable),SqlMigrationModuleOptions,buildSqlQuerierMigrationModule,Sqlite3QuerierPool(toSqliteQuerierPool), the migration builder’sIMigrationBuilder,ITableBuilderand the like (toMigrationBuilder,TableBuilder), and each driver subclass (PgDialect,NeonQuerier,LibsqlQuerier, …) to its base class. MongoQuerier,isMongoQuerierandMongoSchemaIntrospector, imported fromuql-orm/mongodb.- Entries:
uql-orm/maria,uql-orm/mongoanduql-orm/bunSqltouql-orm/mariadb,uql-orm/mongodbanduql-orm/bun-sql;uql-orm/type,uql-orm/dialect,uql-orm/entity,uql-orm/querieranduql-orm/namingStrategytouql-orm. - Removes
import 'reflect-metadata', andexperimentalDecorators/emitDecoratorMetadatafromtsconfig.json.
What it leaves to you
Section titled “What it leaves to you”@Transactional(),@InjectQuerier(),@Log()and@Serialized(), which UQL does not have. Wrap a@Transactional()method’s body inpool.transaction()on the pool you choose.- Removed exports with no one-to-one replacement, such as
setQuerierPoolorAbstractPgQuerier; internals the root no longer exports (AbstractSqlQuerier,fieldOf, …); the migration storages; and any import fromuql-orm/utilthe root does not export (raw,refsand the rest it does export move touql-orm). Migrator’spending()andexecuted(), now read off(await migrator.status()), and a baredown()(worth a look:), which reverts only the last migration:{ step: Infinity }reverts them all.- A
col()whose entity it cannot tell: a raw built apart from its query, one under a relation filter, or a column no field maps. Read it offrefs(Entity). - Values it cannot read statically:
@Field(shared), a spread, a non-literal list. - A partial-index
wheretemplate that interpolates, sincesqlwould bind each interpolation as a value. - A JSON index entry whose
pathis not a string literal, or whosejsonPath/jsonArraytakes an option besidespath,typeandlength. - A
run/allwith values it cannot place safely, such as a non-literal list or SQL, or mixed?and$n. - SQL in a definition that names a column by hand (
worth a look:). Only a ref follows a rename:(user) => sql`lower(${user.email})`. - An
$excludeit cannot read, or one beside a$select: merge it in, each field asfield: false. - A to-one onto a composite key (declare a column per key and pair each in
references), and a to-one whose<relation>Idis not a declared column. - A property whose type maps to no column (
Json<T>, a vector, a mixed union), a key under a computed name, and a nullable column’s property with no written type. - A
@Field/@Idwith notype, and a relation with noentity: declare the column as a builder (f.string()) and name the target (entity: () => Target). Relation<T>, which uql no longer exports: write the property’s type without it, and drop the import.- A definer on a decorated class (
defineIndex(Post, ...)), and@Entity’sfields,relations,hooksandextends: each is a decorator, or the class’s ownextends, now. - A column option it cannot write as a method: a
shape, which a JSON payload no longer takes (write a Standard Schema), or a flag such asunique: isUniquethat no literal states. - A file that already declares a name the codemod would import (
sql,refs,idKey,f, a renamed export), which it leaves unchanged. - A branded string id’s column,
f.uuid()orf.string().f.uuid()is a native column type and may need a migration. target: esnext, which leaves decorators untransformed. Any other modern target works.