Skip to content

Schema Spec v1 #2

@heiwen

Description

@heiwen

Summary

This document defines the v1 schema model for no-orm.

The goal is a minimal, database-independent schema representation that:

  • is small enough to stay practical as a core library
  • can support SQL and non-SQL adapters later
  • avoids backend-specific concepts in the public schema shape
  • is sufficient for current storage needs such as the hebo-gateway conversation storage layer

This spec defines the canonical schema shape directly. There is no separate internal AST in v1.

Goals

  • Provide one canonical schema representation.
  • Keep the schema model portable across databases.
  • Keep the public API small and explicit.
  • Support only the minimum structural concepts needed today.
  • Leave query builders, migrations, and runtime insert behavior outside this spec.

Non-goals

v1 does not include:

  • dedicated schema validation as a first-class feature
  • defaults
  • runtime defaults
  • arbitrary function defaults
  • foreign keys
  • relations
  • enums
  • uniqueness constraints
  • check constraints
  • custom index methods
  • custom backend metadata escape hatches
  • migrations
  • schema diffing
  • introspection
  • separate builder and compiled schema models

Canonical Type Definitions

export type Schema = Record<string, Model>;

export interface Model {
  fields: Record<string, Field>;
  primaryKey: {
    fields: [string, ...string[]];
  };
  indexes?: Index[];
}

export interface Field {
  type: FieldType;
  nullable?: boolean;
}

export type FieldType =
  | { type: "string"; max?: number }
  | { type: "number" }
  | { type: "boolean" }
  | { type: "timestamp" }
  | { type: "json" };

export interface Index {
  fields: IndexField[];
}

export interface IndexField {
  field: string;
  order?: "asc" | "desc";
}

Naming

The schema vocabulary is:

  • Schema
  • Model
  • Field
  • Index

These terms are intended to be more database-independent than table and column, while still remaining familiar to ORM users.

Schema Structure

A schema is a record where the keys are model names.

Example:

const schema: Schema = {
  conversations: {
    fields: {
      id: { type: { type: "string", max: 255 } },
    },
    primaryKey: {
      fields: ["id"],
    },
  },
};

There is no wrapper object such as { models: ... }.

Field Types

v1 supports exactly five field types:

  • string
  • number
  • boolean
  • timestamp
  • json

String

{ type: "string"; max?: number }

Semantics:

  • max is a logical maximum length
  • max is not a guarantee of a particular backend storage type
  • adapters may map it to VARCHAR(n), TEXT, or another appropriate physical representation

Number

{ type: "number" }

Semantics:

  • represents numeric data
  • storage format is adapter-defined
  • examples include SQL integer, float, double, numeric, or another backend-native numeric representation

Boolean

{ type: "boolean" }

Semantics:

  • represents boolean data
  • storage format is adapter-defined
  • examples include SQL BOOLEAN, integer-backed booleans, or another backend-native boolean representation

Timestamp

{ type: "timestamp" }

Semantics:

  • represents a point in time
  • storage format is adapter-defined
  • examples include SQL BIGINT, SQL TIMESTAMP, or another backend-native representation

JSON

{ type: "json" }

Semantics:

  • represents structured JSON-compatible data
  • storage format is adapter-defined
  • examples include JSONB, JSON, TEXT, or a document-native representation

Nullability

Fields are non-nullable by default.

Nullable fields are expressed with:

{
  type: { type: "json" },
  nullable: true,
}

Rules:

  • nullable is optional
  • if omitted, it is treated as false
  • primary key fields must not be nullable

Primary Keys

Each model must define exactly one primary key.

Shape:

primaryKey: {
  fields: [string, ...string[]];
}

Rules:

  • supports both single-field and composite primary keys
  • all referenced fields must exist in fields
  • field names in the primary key must be unique
  • every referenced field must be non-nullable

Examples:

primaryKey: { fields: ["id"] }
primaryKey: { fields: ["conversation_id", "id"] }

Indexes

Indexes are optional and model-level.

Shape:

indexes?: Index[]

Each index is defined as:

{
  fields: [
    { field: "created_at", order: "desc" },
    { field: "id", order: "desc" },
  ],
}

Rules:

  • every referenced field must exist in the model
  • order is optional
  • order, if present, must be "asc" or "desc"
  • index names are not part of the schema in v1
  • index methods are not part of the schema in v1
  • adapters may generate deterministic backend-specific index names internally if needed

Structural Expectations

Even though v1 does not require a dedicated validation layer, adapters may assume:

  • schema keys are model names
  • fields is an object of field definitions
  • primaryKey.fields contains one or more existing field names
  • primary key field names do not repeat
  • string.max, if present, is a positive integer
  • order, if present, is "asc" or "desc"
  • primary key fields are not nullable

Adapter Responsibilities

Adapters consume the canonical schema and map it to backend-specific behavior.

Adapters are responsible for:

  • choosing physical storage types
  • choosing identifier quoting rules
  • emitting primary key syntax
  • emitting index syntax
  • deciding how to represent ordered indexes in the target backend
  • generating backend-specific index names when required

Examples:

  • SQL adapters may map string({ max: 255 }) to VARCHAR(255) or TEXT
  • a Postgres adapter may store json as JSONB
  • a SQLite adapter may store json as TEXT
  • a MongoDB adapter may interpret a model as a collection and a field as a document field

The schema expresses portable intent. Adapters choose backend implementation details.

Design Constraints

The schema intentionally does not encode:

  • whether a model is a SQL table or a document collection
  • whether timestamps are numeric or native datetime values
  • whether JSON is stored natively or as text
  • which index implementation a backend should use

These decisions belong to adapters.

Minimal Schema Bootstrap

To match the current hebo-gateway storage layer, v1 should support minimal schema bootstrap behavior for adapters that can create storage structures.

That means:

  • create models if they do not exist
  • create indexes if needed
  • use backend-specific idempotent DDL where possible

This does not require:

  • migration history
  • schema diffing
  • rollback support
  • generated migration files
  • destructive schema changes

In other words, v1 needs schema bootstrap, not a full migration framework.

Example: Hebo Gateway Storage

This example reflects the current hebo-gateway conversation storage shape.

export const schema: Schema = {
  conversations: {
    fields: {
      id: { type: { type: "string", max: 255 } },
      created_at: { type: { type: "timestamp" } },
      metadata: { type: { type: "json" }, nullable: true },
    },
    primaryKey: {
      fields: ["id"],
    },
    indexes: [
      {
        fields: [
          { field: "created_at", order: "desc" },
          { field: "id", order: "desc" },
        ],
      },
    ],
  },

  conversation_items: {
    fields: {
      id: { type: { type: "string", max: 255 } },
      conversation_id: { type: { type: "string", max: 255 } },
      created_at: { type: { type: "timestamp" } },
      type: { type: { type: "string", max: 64 } },
      data: { type: { type: "json" } },
    },
    primaryKey: {
      fields: ["conversation_id", "id"],
    },
    indexes: [
      {
        fields: [
          { field: "conversation_id" },
          { field: "created_at", order: "desc" },
          { field: "id", order: "desc" },
        ],
      },
    ],
  },
};

Deferred Future Extensions

These may be added later if there is a concrete use case:

  • defaults
  • runtime-generated values
  • more field types such as integer or float
  • uniqueness constraints
  • foreign keys
  • backend hint metadata
  • explicit index naming
  • logical index strategies
  • schema builders on top of the canonical shape

Current Recommendation

Implement v1 directly around this canonical schema representation:

  1. define the exported TypeScript interfaces
  2. implement one or more adapters that consume this shape
  3. support minimal schema bootstrap for adapters that can create backend structures

Do not add a second schema representation unless there is a proven need for it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions