Column types
numeric
Because not every database numeric type can fit into JS number type, some types will be returned as strings.
Numeric types can have numeric operators in WHERE
conditions.
// signed two-byte integer
t.smallint() // -> number
// signed four-byte integer
t.integer() // -> number
// signed eight-byte integer
t.bigint() // -> string
// exact numeric of selectable precision
t.numeric(precision?: number, scale?: number) // -> string
// decimal is an alias for numeric
t.decimal(precision?: number, scale?: number) // -> string
// single-precision floating-point number (4 bytes)
t.real() // -> number
// double-precision floating-point number (8 bytes)
t.doublePrecision() // -> number
// autoincrementing integer (GENERATED BY DEFAULT AS IDENTITY)
t.identity() // -> number
// autoincrementing two-byte integer
t.smallSerial() // -> number
// autoincrementing four-byte integer
t.serial() // -> number
// autoincrementing eight-byte integer
t.bigSerial() // -> string
As listed in code comments above, bigint
, numeric
, decimal
, and bigSerial
have string output.
You can set up parsing to a number
type, (remember this can cause bugs on large numbers):
t.bigint().parse(parseInt);
Or bigint
Postgres type can be parsed to bigint
JavaScript type, but be aware that such values should be explicitly turned to a string when preparing JSON response:
t.bigint().parse(BigInt);
Numeric-type columns support the following where
operators:
db.someTable.where({
numericColumn: {
// lower than
lt: value,
// lower than or equal to
lte: value,
// greater than
gt: value,
// greater than or equal to
gte: value,
// between x and y
between: [x, y],
},
});
text
t.text()
is for an unlimited databasetext
type.t.varchar(limit?: number)
is for text with a limit on a database level. If you don't specify a limit, it is identical to theTEXT
Postgres type.t.string(limit = 255)
is the same asvarchar
with 255 default limit.
Text types can have numeric operators in WHERE
conditions.
// text with unlimited length
t.text() // -> string
// variable-length text with limit
t.varchar(limit?: number) // -> string
// `varchar` type with optional limit defaulting to 255.
t.string(limit?: number = 255) // -> string
The char
database type isn't added because it is discouraged by Postgres.
citext
citext is a database type that behaves almost exactly like text
, but is case-insensitive in all operations.
To use it, first enable citext
extension, create migration:
npm run db new enableCitext
import { change } from '../dbScript';
change(async (db) => {
await db.createExtension('citext');
});
npm run db migrate
And now citext
is available and can be used just as a text
type. It supports the same operators.
// text variable unlimited length
t.citext(); // -> string
tsvector
For full text search: define a generated column for a text vector.
See generated migration method.
// generate a `ts_vector` from other text columns
t.tsvector().generated(['title', 'body']).searchIndex();
tsquery
For full text search to store queries.
// A tsquery value stores lexemes that are to be searched for
t.tsquery(); // -> string
binary
The bytea data type allows storage of binary strings, it is returned as a node.js Buffer object.
t.bytea(); // -> Buffer
date and time
Datetime types can have date operators in WHERE
conditions.
// 4 bytes date (no time of day)
t.date() // -> string
// timestamp with time zone (8 bytes)
t.timestamp(precision?: number) // -> string
// timestamp without time zone (8 bytes), not recommended
t.timestampNoTZ(precision?: number) // -> string
// time without time zone (8 bytes)
// format is 00:00:00
t.time(precision?: number) // -> string
// time with time zone is not added because it should never be used, according to Postgres docs.
Time with time zone is not included because it's discouraged by Postgres docs.
date
, timestamp
, and timestampNoTZ
can be customized with methods asNumber
and asDate
to parse database values into number and JS Date object respectively.
export const BaseTable = createBaseTable({
columnTypes: (t) => ({
...t,
// or use `.asDate()` to work with Date objects
timestamp: () => t.timestamp().asNumber(),
}),
});
// timestamp columns now are returned as numbers, or as Date objects if you choose `asDate`:
const { updatedAt, createdAt } = await db.table.take();
When filtering by timestamp fields, creating or updating records, you can use dates encoded as strings, numbers or Date objects:
// filter, update, create with a Date object:
const date = new Date();
db.table.where({ createdAt: date });
db.table.find(id).update({ ...data, createdAt: date });
db.table.create({ ...data, createdAt: date });
// filter, update, create with a ISO encoded date string
const string = new Date().toISOString();
db.table.where({ createdAt: string });
db.table.find(id).update({ ...data, createdAt: string });
db.table.create({ ...data, createdAt: string });
// filter, update, create with a number retrieved from `getTime`
const number = new Date().getTime();
db.table.where({ createdAt: number });
db.table.find(id).update({ ...data, createdAt: number });
db.table.create({ ...data, createdAt: number });
interval
// interval [ fields ] [ (p) ] 16 bytes time interval -178000000 years 178000000 years 1 microsecond
t.interval(fields?: string, precision?: number) // -> PostgresInterval object
The interval
type takes two optional parameters:
The first parameter is a string containing YEAR
, MONTH
, DAY
, HOUR
, and so on, check the full list in Postgres docs here.
The second parameter specifies the number of fractional digits retained in the second field.
The output of the interval
column is an object containing years
, month
, and other fields:
type Interval = {
years?: number;
months?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
};
const result: Interval = await Table.get('intervalColumn');
boolean
Boolean returns true
or false
.
// 1 byte, true or false
t.boolean(); // -> boolean
UUID
The data type uuid stores Universally Unique Identifiers (UUID).
// UUID stores Universally Unique Identifiers (UUID)
t.uuid(); // -> string, example: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
When using it as a primary key, it will automatically get a gen_random_uuid default.
export class Table extends BaseTable {
readonly table = 'table';
columns = this.setColumns((t) => ({
id: t.uuid().primaryKey(),
name: t.text(),
}));
}
// id is generated in the database
db.table.create({ name: 'Joe' });
To discard the default, use default(null)
:
id: t.uuid().primaryKey().default(null),
If you'd like to use a different default, primaryKey
will respect it:
export class Table extends BaseTable {
readonly table = 'table';
columns = this.setColumns((t) => ({
id: t
.uuid()
.default(() => makeOwnUUID())
.primaryKey(),
name: t.text(),
}));
}
// custom function will be used for the id
db.table.create({ name: 'Joe' });
enum
First argument is the name of an enum in the database, the second is an array of possible values:
export class Table extends BaseTable {
readonly table = 'table';
columns = this.setColumns((t) => ({
enumColumn: t.enum('enumName', ['value1', 'value2', 'value3']),
}));
}
For convenience and to avoid duplication, you can define enum column in columnTypes
of BaseTable
, then reuse it in multiple tables:
export const BaseTable = createBaseTable({
columnTypes: (t) => ({
...t,
orderStatus: () =>
t.enum('orderStatus', ['pending', 'cancelled', 'processed']),
}),
});
export class Table extends BaseTable {
readonly table = 'table';
columns = this.setColumns((t) => ({
// it still can be chained with common column methods
orderStatus: t.orderStatus().nullable(),
}));
}
json
Postgres supports two types of JSON: json
is for storing JSON strings as they were saved, and jsonb
is stored in binary format and allows additional methods.
export class Table extends BaseTable {
readonly table = 'table';
columns = this.setColumns((t) => ({
json: t.jsonText(),
jsonB: t.json(),
}));
}
When using ORM without a validation library, you can set an arbitrary type to json
. Make sure to only save properly validated data.
export class Table extends BaseTable {
readonly table = 'table';
columns = this.setColumns((t) => ({
data: t.json<{
age: number;
name: string;
description: string | null;
tags: string[];
}>(),
}));
}
When having a validation library enabled, json
accepts a callback where you can define a validation schema. If omitted, the type is unknown
.
import { z } from 'zod';
import { object, number, string, optional, array } from 'valibot';
export class Table extends BaseTable {
readonly table = 'table';
columns = this.setColumns((t) => ({
dataZod: t.json(
z.object({
age: z.number(),
name: z.string(),
description: z.string().optional(),
tags: z.string().array(),
}),
),
// or
dataValibot: t.json(
object({
age: number(),
name: string(),
description: optional(string()),
tags: array(string()),
}),
),
}));
}
json
columns support the following where
operators:
db.someTable.where({
jsonColumn: {
// check if the JSON value in the column is a superset of the provided value
jsonSupersetOf: { key: 'value' },
// check if the JSON value in the column is a subset of the provided value
jsonSubsetOf: { key: 'value' },
},
});
When inserting or updating, the json column will use JSON.stringify
to serialize data, except for when null
is passed.
// `data` will be set to a database null, not JSON null:
await db.post.create({ data: null });
await db.post.find(1).update({ data: null });
To insert or update JSON null, provide SQL for this:
import { sql } from './baseTable';
// 'null' is in single quotes
await db.post.create({ data: () => sql`'null'` });
geometry
Geometric types are not parsed and returned as strings as the database returns them.
// point 16 bytes Point on a plane (x,y)
t.point(); // -> string
// line 32 bytes Infinite line {A,B,C}
t.line(); // -> string
// lseg 32 bytes Finite line segment [(x1,y1),(x2,y2)]
t.lseg(); // -> string
// box 32 bytes Rectangular box ((x1,y1),(x2,y2))
t.box(); // -> string
// path 16+16n bytes Closed path (similar to polygon) ((x1,y1),...)
// path 16+16n bytes Open path [(x1,y1),...]
t.path(); // -> string
// polygon 40+16n bytes Polygon (similar to closed path) ((x1,y1),...)
t.polygon(); // -> string
// circle 24 bytes Circle <(x,y),r> (center point and radius)
t.circle(); // -> string
network addresses
// CIDR 7 or 19 bytes IPv4 and IPv6 networks
t.cidr(); // -> string, example: 192.168.100.128/25
// inet 7 or 19 bytes IPv4 and IPv6 hosts and networks
t.inet(); // -> string, example: 192.168.100.128/25
// macaddr 6 bytes MAC addresses
t.macaddr(); // -> string, example: 08:00:2b:01:02:03
// macaddr8 8 bytes MAC addresses (EUI-64 format)
t.macaddr8(); // -> string, example: 08:00:2b:ff:fe:01:02:03
bit string
it strings are strings of 1's and 0's. They can be used to store or visualize bit masks.
// Bit strings are strings of 1's and 0's.
// They can be used to store or visualize bit masks.
// There are two SQL bit types: bit(n) and bit varying(n), where n is a positive integer.
t.bit(); // -> string
// bit varying(n), where n is a positive integer
t.bitVarying(); // -> string
array
// array of another column type
t.array(t.text()); // -> array of argument type
See array operators for WHERE
conditions.
unsupported types
For user-defined custom types, or if some database type is not supported yet, use type
and as
to treat this column as other type:
t.type('type_name').as(t.integer());
domain
Domain is a custom database type that allows to predefine a NOT NULL
and a CHECK
(see postgres tutorial).
In same way as with type
, specify as(otherType)
to treat this column in queries as the other type:
t.domain('domainName').as(t.integer());
money
For currency amount (8 bytes)
t.money(); // -> string, example: '$12.34'
xml
XML data type can be used to store XML data
t.xml(); // -> string
Postgis geography
There's only a very basic support for postgis, open an issue if you need some more.
Here is a geography point type:
t.geography.point();
In the migration the type will have the default 4326
SRID. p The input and output type is:
type PostgisPoint {
lon: number;
lat: number;
srid?: number; // absent for the default 4326
}