📦
Bunty

Table Schemas

Learn how to define type-safe database tables and columns using Bunty’s fluent API. The schema system provides full TypeScript type inference and comprehensive MySQL column support.

Basic Table Definition

Use the mysqlTable method to define tables with type-safe columns:

import { MysqlClient, bigint, varchar, timestamp, boolean } from '@bunty/orm';

const db = new MysqlClient(process.env.DATABASE_URL!);

const users = db.mysqlTable('users', {
  id: bigint('id').primaryKey().autoIncrement(),
  name: varchar('name', { size: 100 }).notNullable(),
  email: varchar('email', { size: 255 }).notNullable().unique(),
  emailVerified: boolean('email_verified').default(false),
  createdAt: timestamp('created_at').notNullable(),
  updatedAt: timestamp('updated_at').notNullable()
});

Column Types

Bunty supports all MySQL column types with full TypeScript inference:

Text Columns

import { varchar, char, text, mediumtext, longtext } from '@bunty/orm';

// Variable-length strings with size limit
varchar('name', { size: 255 })
varchar('email', { size: 320 })  // RFC compliant email length

// Fixed-length strings
char('country_code', { size: 2 })    // ISO country codes
char('currency', { size: 3 })        // ISO currency codes

// Text content of various sizes
text('description')           // Up to 65,535 characters
mediumtext('article')        // Up to 16,777,215 characters  
longtext('content')          // Up to 4,294,967,295 characters

Numeric Columns

import { tinyint, smallint, int, bigint, decimal, float, double } from '@bunty/orm';

// Integer types
tinyint('status')           // -128 to 127 (1 byte)
smallint('priority')        // -32,768 to 32,767 (2 bytes)
int('count')               // -2,147,483,648 to 2,147,483,647 (4 bytes)
bigint('user_id')          // Large integers (8 bytes)

// Unsigned integers
tinyint('rating').unsigned()    // 0 to 255
int('views').unsigned()         // 0 to 4,294,967,295

// Auto-increment primary keys
bigint('id').primaryKey().autoIncrement()

// Decimal types for precise calculations
decimal('price', { precision: 10, scale: 2 })    // For money (123456.78)
decimal('latitude', { precision: 10, scale: 8 }) // For coordinates

// Floating point numbers
float('rating')             // Single precision
double('coordinates')       // Double precision

Date and Time

import { date, time, datetime, timestamp, year } from '@bunty/orm';

// Date only (YYYY-MM-DD)
date('birth_date')
date('event_date')

// Time only (HH:MM:SS)
time('start_time')
time('duration')

// Date and time (YYYY-MM-DD HH:MM:SS)
datetime('event_datetime')
datetime('scheduled_at')

// Timestamp with automatic features
timestamp('created_at').notNullable().defaultNow()
timestamp('updated_at').notNullable().onUpdateCurrentTimestamp()

// Year only (YYYY)
year('graduation_year')

Boolean and Enums

import { boolean, enumerable } from '@bunty/orm';

// Boolean values (stored as TINYINT(1))
boolean('is_active').default(true)
boolean('email_verified').default(false)
boolean('is_premium').notNullable()

// Enumerated values
enumerable('status', ['draft', 'published', 'archived'])
enumerable('priority', ['low', 'medium', 'high', 'urgent'])
enumerable('user_role', ['admin', 'editor', 'user', 'guest'])

JSON and Binary Data

import { json, binary, varbinary, blob } from '@bunty/orm';

// JSON data (MySQL 5.7+)
json('metadata')
json('settings')
json('user_preferences')

// Binary data
binary('file_hash', { size: 32 })      // Fixed-size binary
varbinary('token', { size: 255 })      // Variable-size binary
blob('file_data')                      // Binary large object

Column Modifiers

Constraints

Add constraints to ensure data integrity:

// Primary key constraint
.primaryKey()

// Not null constraint
.notNullable()

// Unique constraint
.unique()

// Auto increment for numeric columns
.autoIncrement()

// Unsigned for numeric columns
.unsigned()

// Example: Comprehensive user ID column
bigint('id')
  .primaryKey()
  .autoIncrement()
  .unsigned()
  .notNullable()

Default Values

Set default values for columns:

// Static default values
varchar('status').default('active')
int('attempts').default(0)
boolean('is_verified').default(false)

// MySQL function defaults
timestamp('created_at').defaultNow()                    // CURRENT_TIMESTAMP
timestamp('updated_at').onUpdateCurrentTimestamp()      // ON UPDATE CURRENT_TIMESTAMP

// Null as default (explicit)
varchar('middle_name').nullable().default(null)

Comments and Documentation

Add comments to document your schema:

// Column comments
varchar('email', { size: 255 })
  .notNullable()
  .unique()
  .comment('User email address for authentication')

// Multiple columns with comments
const users = db.mysqlTable('users', {
  id: bigint('id')
    .primaryKey()
    .autoIncrement()
    .comment('Unique user identifier'),
    
  email: varchar('email', { size: 255 })
    .notNullable()
    .unique()
    .comment('User email for login and notifications'),
    
  passwordHash: varchar('password_hash', { size: 255 })
    .notNullable()
    .comment('Bcrypt hashed password'),
    
  emailVerifiedAt: timestamp('email_verified_at')
    .nullable()
    .comment('Timestamp when email was verified')
});

Table Configuration

Configure table-level options:

const posts = db.mysqlTable('posts', {
  id: bigint('id').primaryKey().autoIncrement(),
  title: varchar('title', { size: 200 }).notNullable(),
  content: text('content').notNullable(),
  createdAt: timestamp('created_at').notNullable(),
  updatedAt: timestamp('updated_at').notNullable()
})
  .engine('InnoDB')                           // Storage engine
  .charset('utf8mb4')                         // Character set
  .collation('utf8mb4_unicode_ci')           // Collation
  .comment('Blog posts and articles');       // Table comment

Storage Engines

Choose the appropriate storage engine:

// InnoDB (recommended for most use cases)
.engine('InnoDB')    // ACID compliance, foreign keys, row-level locking

// MyISAM (for read-heavy, non-transactional data)
.engine('MyISAM')    // Fast reads, table-level locking

// Memory (for temporary/cache tables)
.engine('MEMORY')    // In-memory storage

Advanced Schema Patterns

Timestamps Pattern

Create a reusable timestamps pattern:

function withTimestamps() {
  return {
    createdAt: timestamp('created_at').notNullable().defaultNow(),
    updatedAt: timestamp('updated_at').notNullable().onUpdateCurrentTimestamp()
  };
}

const users = db.mysqlTable('users', {
  id: bigint('id').primaryKey().autoIncrement(),
  name: varchar('name', { size: 100 }).notNullable(),
  email: varchar('email', { size: 255 }).notNullable().unique(),
  ...withTimestamps()
});

Soft Deletes Pattern

Implement soft deletes with a deleted timestamp:

function withSoftDeletes() {
  return {
    deletedAt: timestamp('deleted_at').nullable().comment('Soft delete timestamp')
  };
}

const posts = db.mysqlTable('posts', {
  id: bigint('id').primaryKey().autoIncrement(),
  title: varchar('title', { size: 200 }).notNullable(),
  content: text('content').notNullable(),
  ...withTimestamps(),
  ...withSoftDeletes()
});

UUID Primary Keys

Use UUIDs instead of auto-increment IDs:

import { char } from '@bunty/orm';

const sessions = db.mysqlTable('sessions', {
  id: char('id', { size: 36 }).primaryKey(),  // UUID v4
  userId: bigint('user_id').notNullable(),
  token: varchar('token', { size: 255 }).notNullable().unique(),
  expiresAt: timestamp('expires_at').notNullable(),
  ...withTimestamps()
});

Real-World Examples

E-commerce Product Table

const products = db.mysqlTable('products', {
  id: bigint('id').primaryKey().autoIncrement(),
  sku: varchar('sku', { size: 50 }).notNullable().unique(),
  name: varchar('name', { size: 255 }).notNullable(),
  description: text('description'),
  price: decimal('price', { precision: 10, scale: 2 }).notNullable(),
  compareAtPrice: decimal('compare_at_price', { precision: 10, scale: 2 }).nullable(),
  cost: decimal('cost', { precision: 10, scale: 2 }).nullable(),
  weight: decimal('weight', { precision: 8, scale: 3 }).nullable(),
  status: enumerable('status', ['draft', 'active', 'archived']).default('draft'),
  isDigital: boolean('is_digital').default(false),
  requiresShipping: boolean('requires_shipping').default(true),
  inventory: int('inventory').unsigned().default(0),
  minInventory: int('min_inventory').unsigned().default(0),
  metadata: json('metadata').nullable(),
  ...withTimestamps()
})
  .engine('InnoDB')
  .comment('Product catalog');

User Authentication Table

const users = db.mysqlTable('users', {
  id: bigint('id').primaryKey().autoIncrement(),
  email: varchar('email', { size: 320 }).notNullable().unique(),
  passwordHash: varchar('password_hash', { size: 255 }).notNullable(),
  firstName: varchar('first_name', { size: 100 }).notNullable(),
  lastName: varchar('last_name', { size: 100 }).notNullable(),
  displayName: varchar('display_name', { size: 200 }).nullable(),
  avatar: varchar('avatar', { size: 500 }).nullable(),
  timezone: varchar('timezone', { size: 50 }).default('UTC'),
  locale: varchar('locale', { size: 10 }).default('en'),
  role: enumerable('role', ['admin', 'editor', 'user']).default('user'),
  isActive: boolean('is_active').default(true),
  emailVerifiedAt: timestamp('email_verified_at').nullable(),
  lastLoginAt: timestamp('last_login_at').nullable(),
  passwordChangedAt: timestamp('password_changed_at').nullable(),
  ...withTimestamps(),
  ...withSoftDeletes()
})
  .engine('InnoDB')
  .comment('User accounts and authentication');

Blog Posts with Categories

const categories = db.mysqlTable('categories', {
  id: bigint('id').primaryKey().autoIncrement(),
  name: varchar('name', { size: 100 }).notNullable(),
  slug: varchar('slug', { size: 150 }).notNullable().unique(),
  description: text('description').nullable(),
  parentId: bigint('parent_id').nullable(),
  sortOrder: int('sort_order').unsigned().default(0),
  isActive: boolean('is_active').default(true),
  ...withTimestamps()
});

const posts = db.mysqlTable('posts', {
  id: bigint('id').primaryKey().autoIncrement(),
  authorId: bigint('author_id').notNullable(),
  categoryId: bigint('category_id').nullable(),
  title: varchar('title', { size: 255 }).notNullable(),
  slug: varchar('slug', { size: 300 }).notNullable().unique(),
  excerpt: varchar('excerpt', { size: 500 }).nullable(),
  content: longtext('content').notNullable(),
  featuredImage: varchar('featured_image', { size: 500 }).nullable(),
  status: enumerable('status', ['draft', 'published', 'scheduled', 'archived']).default('draft'),
  publishedAt: timestamp('published_at').nullable(),
  scheduledAt: timestamp('scheduled_at').nullable(),
  viewCount: int('view_count').unsigned().default(0),
  commentCount: int('comment_count').unsigned().default(0),
  metadata: json('metadata').nullable(),
  ...withTimestamps(),
  ...withSoftDeletes()
})
  .engine('InnoDB')
  .comment('Blog posts and articles');

TypeScript Integration

The schema system provides full TypeScript type inference:

// TypeScript automatically infers the types
type User = {
  id: number;
  email: string;
  passwordHash: string;
  firstName: string;
  lastName: string;
  displayName: string | null;
  role: 'admin' | 'editor' | 'user';
  isActive: boolean;
  emailVerifiedAt: Date | null;
  createdAt: Date;
  updatedAt: Date;
};

// Use with query results
const user: User = await db.select().from(users).where('id', '=', 1).first();

Best Practices

Naming Conventions

// Use snake_case for database columns
// Use camelCase for TypeScript properties (auto-converted)

const users = db.mysqlTable('users', {
  id: bigint('id').primaryKey().autoIncrement(),
  firstName: varchar('first_name', { size: 100 }),  // first_name in DB, firstName in TS
  lastName: varchar('last_name', { size: 100 }),    // last_name in DB, lastName in TS
  emailAddress: varchar('email_address', { size: 255 }) // email_address in DB, emailAddress in TS
});

Index Considerations

// Primary keys automatically get clustered indexes
// Unique constraints automatically get unique indexes
// Consider adding indexes for frequently queried columns

const users = db.mysqlTable('users', {
  id: bigint('id').primaryKey().autoIncrement(),      // Clustered index
  email: varchar('email', { size: 255 }).unique(),   // Unique index
  status: enumerable('status', ['active', 'inactive']) // Consider index if filtering frequently
});

Data Type Selection

Choose appropriate data types for performance and storage:

// Use the smallest appropriate data type
tinyint('age')              // vs int('age') - saves 3 bytes per row
varchar('title', { size: 200 }) // vs text('title') - better performance for known max length

// Use unsigned for non-negative values
int('view_count').unsigned()    // Doubles the positive range

// Use appropriate decimal precision
decimal('price', { precision: 10, scale: 2 })  // $99,999,999.99 max
decimal('tax_rate', { precision: 5, scale: 4 }) // 9.9999% max rate

This comprehensive guide covers all aspects of defining robust, type-safe database schemas with the Bunty ORM. The schema system provides the foundation for your application’s data layer with full TypeScript support.

Have questions? Join our Discord community
Found an issue? Edit this page on GitHub