Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

L2 - Scalars

Scalar type definitions with State generic for compile-time tracking

Location: src/schema/scalars/

Why This Layer Exists

Scalars need to preserve type information as users chain modifiers:

s.string()           // StringScalar<{type: "string"}>
  .nullable()        // StringScalar<{type: "string", nullable: true}>  
  .default("hello")  // StringScalar<{..., default: "hello"}>

Each modifier returns a new instance with updated types. TypeScript tracks this through the State generic, enabling fully typed queries without code generation.

The State Generic Pattern

Every scalar carries its configuration as a type parameter:

class StringScalar<S extends StringScalarState> {
  // S contains: type, nullable, optional, default, unique, id, etc.
}

When you call .nullable(), a new scalar is created:

nullable(): StringScalar<S & { nullable: true }> {
  return new StringScalar({ ...this.state, nullable: true });
}

The key insight: immutability enables type tracking. If we mutated the scalar, TypeScript couldn’t know the type changed.

Available Scalar Types

Factory Database Type TypeScript Type
s.string() VARCHAR/TEXT string
s.int() INTEGER number
s.bigInt() BIGINT bigint
s.float() FLOAT/DOUBLE number
s.decimal() DECIMAL string
s.boolean() BOOLEAN boolean
s.dateTime() DATETIME/TIMESTAMP Date
s.date() DATE string (ISO date)
s.time() TIME string (ISO time)
s.json<T>() JSON/JSONB T
s.enum(values) ENUM/VARCHAR values[number]
s.blob() BLOB/BYTEA Uint8Array
s.point() POINT { x, y }
s.vector(dim) VECTOR number[]

Common Modifiers

Modifiers work across scalar types where applicable:

Modifier Purpose
.nullable() Allow NULL values
.default(value) Set default value
.id() Mark as primary key
.unique() Add unique constraint
.map(columnName) Map to different column name
.uuid() Auto-generate UUIDs (string)
.ulid() Auto-generate ULIDs (string)

Lazy Schema Building

Scalars don’t build validation schemas immediately - that would be slow. Instead, schemas are built on first access:

// Internal pattern
get ["~"]() {
  this._schemas ??= buildSchemas(this.state);  // Built once, cached
  return this._schemas;
}

This matters because VibORM creates many scalar instances during schema definition, but only builds validation schemas when actually needed for queries.

Connection to Other Layers

  • L1 (Validation): Scalars use v.* primitives internally
  • L3 (Query Schemas): Query schemas compose scalar schemas for where/create/update
  • L7 (Adapters): Scalar types determine SQL column types
  • L12 (Migrations): Scalar state drives DDL generation

Was this page helpful?