@db4/schema
v0.1.2
Published
IceType schema compiler for db4 - concise, human-readable schema language with TypeScript inference
Maintainers
Readme
@db4/schema
Your schema is lying to you.
Types in TypeScript. Migrations in SQL. Validation scattered everywhere. When they disagree, you get runtime errors.
@db4/schema ends the chaos. Define once with IceType. Get types, migrations, and validation that never drift.
The Problem
Every database project follows the same path to pain:
- Write TypeScript interfaces
- Write SQL migrations
- Add validation at API boundaries
- Watch them slowly diverge
- Debug 3am production errors because
emailwas supposed to be required - Repeat forever
Teams make it worse. Someone updates the types, forgets the migration. Someone else edits the database directly. Your "single source of truth" becomes three sources of fiction.
The Solution: IceType
One schema. Every output. Zero drift.
import { parseSchema, defineSchema, InferType } from '@db4/schema'
// Full IceType syntax
const User = parseSchema({
$type: 'User',
id: 'uuid!',
email: 'string! #unique',
name: 'string!',
avatar: 'string?',
createdAt: 'timestamp! = now()',
posts: '[Post] -> author',
})
// Simple syntax with type inference
const userSchema = defineSchema('User', {
id: 'uuid',
email: 'string',
name: 'string',
avatar: 'string?',
age: 'int?',
})
type User = InferType<typeof userSchema>
// { id: string; email: string; name: string; avatar?: string; age?: number }Quick Start
1. Install
npm install @db4/schema2. Define
import { parseSchema, defineSchema, SchemaRegistry } from '@db4/schema'
const PostSchema = parseSchema({
$type: 'Post',
id: 'uuid!',
title: 'string!',
content: 'text!',
published: 'boolean = false',
authorId: 'uuid! #index',
tags: '[string]?',
viewCount: 'int = 0',
embedding: 'vector[1536] ~> content', // AI auto-embed
createdAt: 'timestamp! = now()',
$fts: ['title', 'content'], // Full-text search
})
const TagSchema = defineSchema('Tag', {
id: 'uuid',
name: 'string',
color: 'string?',
})3. Validate
const registry = new SchemaRegistry()
registry.register(userSchema)
// Throws on invalid data
registry.validate('User', { id: '123', email: '[email protected]', name: 'Alice' })
// Or get detailed results
const result = registry.validateWithResult('User', someData)
if (!result.valid) {
console.log(result.errors)
}IceType Syntax
Reads like documentation. Compiles to everything.
Types
| Type | Description | TypeScript |
|------|-------------|------------|
| string | Text | string |
| text | Long text | string |
| int | Integer | number |
| float | Decimal | number |
| boolean | True/false | boolean |
| uuid | UUID string | string |
| timestamp | Date/time | Date |
| date | Date only | Date |
| time | Time only | Date |
| json | Any JSON | unknown |
| binary | Binary data | ArrayBuffer |
Modifiers
{
required: 'string!', // Required (!)
optional: 'string?', // Optional (?)
indexed: 'string #index', // Database index
unique: 'string! #unique', // Unique constraint
array: '[string]', // Array
withDefault: 'int = 0', // Default value
}Parametric Types
{
price: 'decimal(10,2)', // Precision
code: 'varchar(10)', // Fixed length
embedding: 'vector[1536]', // Dimensions
}Relations
{
comments: '[Comment] -> post', // Has many
post: 'Post! <- comments', // Belongs to
similar: '~> content', // AI semantic match
}Computed Fields
{
firstName: 'string!',
lastName: 'string!',
fullName: 'string := firstName ++ " " ++ lastName',
displayName: 'string := nickname ?? firstName',
}Directives
{
$type: 'User', // Schema name
$partitionBy: ['tenantId'], // Partition key
$fts: ['title', 'content'], // Full-text search
$vector: { embedding: 1536 }, // Vector index
$index: [['email', 'tenantId']], // Composite indexes
}Schema Registry
Manages schemas with validation caching and lazy loading:
import { SchemaRegistry, defineSchema } from '@db4/schema'
const registry = new SchemaRegistry()
registry.register(userSchema)
registry.register(postSchema)
// Lazy loading for circular dependencies
registry.registerLazy('Comment', () => defineSchema('Comment', {
id: 'uuid',
content: 'string',
authorId: 'uuid',
postId: 'uuid',
}))
// Full validation details
const result = registry.validateWithResult('User', data)
// { valid: false, errors: [...], fieldsValidated: 5, durationMs: 2 }
// Cached validation (same object = instant)
registry.isValid('User', cachedUser)Migrations
Generate migrations from schema changes:
import { diffSchemas, generateMigration, MigrationRunner } from '@db4/schema'
const diff = diffSchemas(oldSchema, newSchema)
if (diff.hasDestructiveChanges) {
console.warn('Warning:', diff.warnings)
}
const migration = generateMigration(diff, {
dialect: 'sqlite', // or 'postgresql', 'mysql', 'd1'
safeMode: true,
})
console.log(migration.up) // ALTER TABLE statements
console.log(migration.down) // Rollback statements
const runner = new MigrationRunner(executor, { dialect: 'sqlite' })
await runner.initialize()
await runner.runPending(migrations)Type Inference
Full TypeScript types from schema definitions:
import { defineSchema, InferType } from '@db4/schema'
const userSchema = defineSchema('User', {
id: 'uuid',
email: 'string',
name: 'string',
age: 'int?',
tags: 'string[]',
metadata: 'json?',
})
type User = InferType<typeof userSchema>
// {
// id: string
// email: string
// name: string
// age?: number
// tags: string[]
// metadata?: unknown
// }Schema Discovery
Infer schemas from existing data:
import { inferType, inferSchema, discoverSchema } from '@db4/schema'
inferType('550e8400-e29b-41d4-a716-446655440000') // 'uuid'
inferType(42) // 'int'
inferType('2024-01-15T10:30:00Z') // 'timestamp'
const docs = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob' }, // email missing
]
const schema = inferSchema(docs)
// { id: 'int', name: 'string', email: 'string?' }Victory
With a unified schema:
- TypeScript catches errors at compile time, not 3am
- Migrations generate automatically
- Validation stays in sync forever
- Teams ship faster with fewer bugs
- Sleep through the night
Failure
Without one:
- Runtime errors from type mismatches
- Manual migrations that drift from reality
- Validation duplicated and inconsistent
- "Worked yesterday" becomes your morning greeting
- Tech debt compounds with every feature
API Reference
Parsing
parseField(fieldDef)- Parse a field definitionparseSchema(definition)- Parse a full schemaparseRelation(relDef)- Parse a relationtokenize(input)- Tokenize IceType syntax
Schema Definition
defineSchema(name, fields)- Create a type-safe schemaInferType<S>- Infer TypeScript type from schema
Validation
SchemaRegistry- Schema management with cachingLazySchema- Deferred schema loading
Migrations
diffSchemas(from, to)- Compare IceType schemasdiffSimpleSchemas(from, to)- Compare simple schemasgenerateMigration(diff, options)- Generate migration SQLMigrationRunner- Execute and track migrationsMigrationTracker- Track applied migrations
Computed Fields
parseComputedExpression(expr)- Parse computed syntaxextractDependencies(ast)- Get field dependenciescheckDeterministic(ast)- Check if expression is puredetectCircularDependencies(fields)- Find cyclesgetComputationOrder(fields)- Topological sortvalidateComputedDependencies(computed, fields)- Validate dependencies
Utilities
clearParserCaches()- Clear memoization cachesgenerateMigrationId()- Generate timestamp-based IDcomputeChecksum(migration)- Compute migration checksumgenerateMigrationTableSQL(dialect)- Generate tracking table DDL
License
MIT
