Skip to main content

Defining SQL Models

Models represent tables in your database. Define them using the defineModel function with the col and rel namespaces. For the full API reference including nullable-aware types, generics, and typed serialize/prepare, see Programmatic Models (defineModel).

Example: User Model

import { defineModel, col } from "hysteria-orm";

const Post = defineModel("posts", {
columns: {
id: col.increment(),
title: col.string(),
body: col.text(),
userId: col.integer(),
createdAt: col.datetime({ autoCreate: true }),
updatedAt: col.datetime({ autoCreate: true, autoUpdate: true }),
},
});

const Address = defineModel("addresses", {
columns: {
id: col.increment(),
street: col.string(),
city: col.string(),
},
});

const UserAddress = defineModel("user_addresses", {
columns: {
id: col.increment(),
userId: col.integer(),
addressId: col.integer(),
},
});

const User = defineModel("users", {
columns: {
id: col.increment(),
name: col.string(),
email: col.string({ nullable: false }),
password: col.string({ hidden: true }),
status: col.enum(["active", "inactive"] as const),
isActive: col.boolean(),
balance: col.decimal({ precision: 10, scale: 2 }),
metadata: col.json(),
createdAt: col.datetime({ autoCreate: true }),
updatedAt: col.datetime({ autoCreate: true, autoUpdate: true }),
},
});

Relations are defined in a separate schema file — see Relations below.


Column Types

All column types are available via the col namespace. Each returns a ColumnDef with full TypeScript inference.

MethodBase TypeDescription
col()any (user-defined via generic)Generic column. Accepts options like primaryKey, hidden, etc.
col.primary()string | numberGeneric primary key column.
col.increment()numberAuto-incrementing integer primary key (always non-nullable).
col.bigIncrement()numberAuto-incrementing bigint primary key (always non-nullable).
col.integer()numberInteger column.
col.bigInteger()numberBig integer column. Handles Postgres string-to-Number conversion.
col.float()numberFloat column.
col.decimal()numberDecimal column with optional precision and scale.
col.string()stringVARCHAR column with optional length.
col.text()stringLONGTEXT column for longer text content.
col.boolean()booleanBoolean column, handles DB-specific formats (e.g., MySQL tinyint).
col.json()unknown (customizable via generic)JSON/JSONB column.
col.date()DateDATE column (YYYY-MM-DD).
col.datetime()DateDATETIME column (YYYY-MM-DD HH:mm:ss), with auto-creation and auto-update.
col.timestamp()DateUnix timestamp column, with auto-creation and auto-update.
col.time()DateTIME column (HH:mm:ss), with auto-creation and auto-update.
col.uuid()stringAuto-generates a UUID if not provided.
col.ulid()stringAuto-generates a ULID if not provided.
col.binary()Buffer | Uint8Array | stringBinary/blob column.
col.enum(values)values[number]Enum column constrained to the provided values array.
col.encryption.symmetric(opts)stringEncrypts/decrypts value using a symmetric key.
col.encryption.asymmetric(opts)stringEncrypts/decrypts value using asymmetric keys.

Nullable-Aware Type Inference

  • Primary key columns (col.increment(), col.bigIncrement(), col.primary(), col.uuid({ primaryKey: true })) are non-nullable.
  • Non-PK columns are nullable by default — the type includes | null | undefined. Use { nullable: false } to make them required.
const Product = defineModel("products", {
columns: {
id: col.increment(), // number (non-nullable)
name: col.string({ nullable: false }), // string (required)
description: col.string(), // string | null | undefined
price: col.decimal({ precision: 10, scale: 2, nullable: false }), // number (required)
status: col.enum(["draft", "published"] as const), // "draft" | "published" | null | undefined
},
});

Date/Time Column Types

Choose the appropriate date/time column type based on your database column:

MethodDatabase Column TypeFormatUse CaseMigration Example
col.date()DATEYYYY-MM-DDBirth dates, event dates (no time)table.date('birth_date')
col.datetime()DATETIME, DATETIME2YYYY-MM-DD HH:mm:ssCreated/updated timestampstable.timestamp('created_at')
col.timestamp()TIMESTAMP (as integer)Unix timestamp (e.g., 1766152251)High-performance timestamps as integerstable.integer('last_login')
col.time()TIMEHH:mm:ssStart/end times, durations (no date)table.time('start_time')

Important Notes:

  • When using table.timestamp() in migrations, it creates DATETIME/DATETIME2 columns (not Unix timestamp integers). Use col.datetime() for these columns.
  • For actual Unix timestamp integers, define the column as table.integer() or table.bigint() and use col.timestamp() in your model.
  • All date/time column types support autoCreate and autoUpdate options for automatic timestamp management. For autoUpdate, MySQL/MariaDB use the native ON UPDATE CURRENT_TIMESTAMP; on other databases, migrations automatically generate an update trigger (trigger name: trg_{table}_{column}_auto_update).
  • All date/time column types support the timezone option ('UTC' or 'LOCAL').
const Event = defineModel("events", {
columns: {
id: col.increment(),
birthDate: col.date(), // DATE (YYYY-MM-DD)
createdAt: col.datetime({ autoCreate: true }), // DATETIME (auto-set on create)
lastModified: col.timestamp({ autoCreate: true, autoUpdate: true }), // Unix timestamp
businessHoursStart: col.time(), // TIME (HH:mm:ss)
},
});

Column Options

Pass options to any col method to control column behavior:

OptionTypeDefaultDescription
primaryKeybooleanfalseMarks this column as the primary key. Only one primary key is allowed per model.
serializefunctionundefinedFunction to transform the value after reading from the database (e.g., parse, convert type).
preparefunctionundefinedFunction to transform the value before writing to the database (e.g., format, encrypt).
hiddenbooleanfalseIf true, this column will not appear in serialized output (e.g., API responses).
autoUpdatebooleanfalseIf true, prepare will always be called on update, even if the value is not in the payload.
databaseNamestringproperty name (case-converted)Custom name for the column in the database.
nullablebooleantrue (false for PK columns)If false, the column cannot be null.
defaultstring | number | null | booleanundefinedThe default value for the column in the database.
const User = defineModel("users", {
columns: {
id: col.integer({
primaryKey: true,
hidden: true,
databaseName: "user_id",
}),
name: col.string({
prepare: (value) => value.trim(),
serialize: (value) => value.toUpperCase(),
}),
status: col.string({ default: "active" }),
},
});

Notes:

  • primaryKey: Composite primary keys are not supported. Defining more than one will throw an error.
  • hidden: Useful for sensitive fields like passwords.
  • prepare/serialize: Use for custom transformations, e.g., encryption, formatting, or type conversion.
  • databaseName: Use if your DB column name differs from your property name or case convention.

Relations

Relations are not defined inside defineModel. Use defineRelations + createSchema in a dedicated schema file instead. This approach works for all project sizes and eliminates circular import issues entirely.

See Defining Relations for the full API and step-by-step example.

HelperDescriptionForeign Key Location
hasOneOne-to-one relationshipOn the related model
hasManyOne-to-many relationshipOn the related model
belongsToInverse of hasOne/hasManyOn the current model
manyToManyMany-to-many via join tableOn the join/pivot table
import { createSchema, defineRelations } from "hysteria-orm";
import { User } from "./user";
import { Post } from "./post";

const UserRelations = defineRelations(User, ({ hasMany }) => ({
posts: hasMany(Post, { foreignKey: "userId" }),
}));

const PostRelations = defineRelations(Post, ({ belongsTo }) => ({
user: belongsTo(User, { foreignKey: "userId" }),
}));

export const schema = createSchema(
{ users: User, posts: Post },
{ users: UserRelations, posts: PostRelations },
);

export const UserModel = schema.users;
export const PostModel = schema.posts;

Table Constraints

Pass indexes, uniques, and checks arrays to defineModel. Column references are type-checked against your columns definition.

const User = defineModel("users", {
columns: {
id: col.increment(),
name: col.string(),
email: col.string({ nullable: false }),
age: col.integer(),
status: col.string(),
},
indexes: [
["email"], // simple array form
{ columns: ["name", "email"], name: "idx_name_email" }, // object form with custom name
],
uniques: [["email"], { columns: ["email"], name: "uq_users_email" }],
checks: [
"age >= 18", // string form
{
expression: "status IN ('active', 'inactive', 'banned')",
name: "chk_status",
}, // object form
],
});
Constraint names

Always provide explicit constraint names for production models. Auto-generated names are deterministic but less readable. Explicit names make migration history and database debugging easier.


Model Options

Customize model behavior via the options key in defineModel:

OptionDescriptionDefault
softDeleteColumnColumn name used for soft deletes."deletedAt"
modelCaseConventionCase convention for model properties.Auto-detected
databaseCaseConventionCase convention for database columns.Auto-detected

See Case Conventions for detailed information on case conversion.