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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions