Skip to content
NewComposite primary keys5 min read

Computed Fields

@Field({ computed }) declares a value the database produces, not one the caller writes. It never takes part in an insert or an update, and reads like any other field.

Reach for one when the value is an expression over the row that no clause covers - arithmetic, a string built from two columns, a date part. Counting a relation is not one of those: use $count, which needs no field on the entity and works on MongoDB too.

import { col, Entity, Id, Field, raw } from 'uql-orm';
@Entity()
export class Product {
@Id({ type: Number })
id?: number;
@Field({ type: Number })
cost?: number;
@Field({ type: Number })
salePrice?: number;
@Field({
type: Number,
// `col` qualifies and escapes each column, which is what keeps the expression
// correct once the query joins something.
computed: raw`${col('salePrice')} - ${col('cost')}`,
})
profit?: number;
}

By default nothing is persisted: the expression is spliced into every statement that reads the field. Add stored: true and it becomes a real column the engine keeps up to date, so it can be indexed, constrained and read without recomputing.

@Field({ type: Number, computed: raw`"qty" * "price"`, stored: true })
total?: number;

Queries do not change. $select, $where and $sort read the field the same way either side of the dial, so stored is something you flip after profiling without touching a call site.

unstored (default) stored: true
where the value is recomputed per statement a column, written by the engine
DDL none GENERATED ALWAYS AS (...) STORED
indexable no yes, like any column
expression anything the dialect parses must be deterministic, over the row
MongoDB yes no - SQL engines only

A stored column needs a type, since a migration has to spell one out. Support is Postgres 12+, MySQL 5.7+, MariaDB 5.2+, SQLite 3.31+.

Computed fields behave exactly like regular fields. You can select them or filter by them.

You write
const products = await pool.findMany(Product, {
$select: { id: true, profit: true },
});
SELECT "id", "salePrice" - "cost" "profit" FROM "Product"
You write
const products = await pool.findMany(Product, {
$select: { id: true },
$where: {
profit: { $gte: 10 },
},
});
SELECT "id" FROM "Product" WHERE "salePrice" - "cost" >= $1