or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

database-management.mderror-handling.mdevents.mdindex.mdlive-queries.mdquery-building.mdschema-management.mdtable-operations.mdutility-functions.md
tile.json

schema-management.mddocs/

Schema Management

Database versioning, schema definition, and migration management for evolving database structures over time.

Capabilities

Version Definition

Creates a new database version with schema definition.

/**
 * Defines a database schema version
 * @param versionNumber - Version number (must be sequential, starting from 1)
 * @returns Version instance for defining schema and upgrade functions
 */
version(versionNumber: number): Version;

interface Version {
  /** Define table schemas for this version */
  stores(schema: { [tableName: string]: string | null }): Version;
  /** Define upgrade function for migrating from previous version */
  upgrade(upgradeFunction: (trans: Transaction) => PromiseLike<any> | void): Version;
}

Usage Examples:

// Basic version definition
db.version(1).stores({
  friends: "++id, name, age",
  messages: "++id, friendId, message, timestamp"
});

// Multiple versions
db.version(1).stores({
  users: "++id, name, email"
});

db.version(2).stores({
  users: "++id, name, email, createdAt", // Add index
  posts: "++id, userId, title, content"    // Add table
});

db.version(3).stores({
  users: "++id, name, email, createdAt",
  posts: "++id, userId, title, content, publishedAt", // Add index
  comments: "++id, postId, userId, content"           // Add table
});

Schema Syntax

The schema string defines table structure using a specific syntax.

Primary Key Definitions

// Schema syntax patterns:
// "++keyName" - Auto-incrementing primary key
// "keyName" - Non-auto-incrementing primary key
// "&keyName" - Unique key (not primary key)

Usage Examples:

// Auto-incrementing primary key
db.version(1).stores({
  users: "++id, name, email"  // id will auto-increment
});

// Non-auto-incrementing primary key
db.version(1).stores({
  settings: "key, value"      // key is the primary key (string)
});

// Unique constraint (not primary key)
db.version(1).stores({
  users: "id, &email, name"   // email must be unique
});

// Compound primary key
db.version(1).stores({
  userRoles: "[userId+roleId], assignedAt" // Compound primary key
});

Index Definitions

// Index syntax patterns:
// "primaryKey, index1, index2" - Simple indexes
// "primaryKey, *multiIndex" - Multi-entry index (for arrays)
// "primaryKey, [compound+index]" - Compound index
// "primaryKey, &uniqueIndex" - Unique index

Usage Examples:

// Simple indexes
db.version(1).stores({
  products: "++id, name, category, price"
});

// Multi-entry index (for array values)
db.version(1).stores({
  articles: "++id, title, *tags, publishedAt"  // tags is an array
});

// Compound indexes
db.version(1).stores({
  orders: "++id, customerId, [customerId+date], status"
});

// Mixed index types
db.version(1).stores({
  users: "++id, &email, name, *interests, [city+state]"
});

Schema Evolution

Managing database schema changes across versions.

Adding Tables

/**
 * Add new tables in later versions
 */

Usage Examples:

// Version 1: Initial schema
db.version(1).stores({
  users: "++id, name, email"
});

// Version 2: Add new table
db.version(2).stores({
  users: "++id, name, email",
  posts: "++id, userId, title, content, createdAt"  // New table
});

// Version 3: Add another table
db.version(3).stores({
  users: "++id, name, email",
  posts: "++id, userId, title, content, createdAt",
  comments: "++id, postId, userId, content, createdAt"  // Another new table
});

Modifying Indexes

/**
 * Add or remove indexes on existing tables
 */

Usage Examples:

// Version 1: Basic indexes
db.version(1).stores({
  products: "++id, name, category"
});

// Version 2: Add price index
db.version(2).stores({
  products: "++id, name, category, price"  // Add price index
});

// Version 3: Add compound index
db.version(3).stores({
  products: "++id, name, category, price, [category+price]"  // Add compound index
});

// Version 4: Remove an index
db.version(4).stores({
  products: "++id, name, price, [category+price]"  // Remove category index
});

Deleting Tables

/**
 * Delete tables by setting schema to null
 */

Usage Examples:

// Version 1: Multiple tables
db.version(1).stores({
  users: "++id, name, email",
  tempData: "++id, data, timestamp",
  posts: "++id, userId, title"
});

// Version 2: Remove temp table
db.version(2).stores({
  users: "++id, name, email",
  tempData: null,  // Delete this table
  posts: "++id, userId, title"
});

Upgrade Functions

Define data migration logic when upgrading between versions.

/**
 * Defines an upgrade function for migrating data
 * @param upgradeFunction - Function to execute during upgrade
 * @returns Version instance for method chaining
 */
upgrade(upgradeFunction: (trans: Transaction) => PromiseLike<any> | void): Version;

interface Transaction {
  /** Database reference */
  db: Dexie;
  /** Transaction mode */
  mode: IDBTransactionMode;
  /** Get table bound to this transaction */
  table(tableName: string): Table<any, any>;
}

Usage Examples:

// Version 1: Initial schema
db.version(1).stores({
  users: "++id, name, email"
});

// Version 2: Add timestamp, migrate existing data
db.version(2)
  .stores({
    users: "++id, name, email, createdAt"
  })
  .upgrade(trans => {
    // Add createdAt to all existing users
    return trans.table("users").toCollection().modify(user => {
      user.createdAt = Date.now();
    });
  });

// Version 3: Restructure data
db.version(3)
  .stores({
    users: "++id, name, email, createdAt, profile",
    profiles: "++id, userId, bio, avatar"
  })
  .upgrade(async trans => {
    // Move profile data to separate table
    const users = await trans.table("users").toArray();
    const profiles = users.map(user => ({
      userId: user.id,
      bio: user.bio || "",
      avatar: user.avatar || null
    }));
    
    // Add profiles
    await trans.table("profiles").bulkAdd(profiles);
    
    // Remove old fields from users
    await trans.table("users").toCollection().modify(user => {
      delete user.bio;
      delete user.avatar;
    });
  });

// Version 4: Data transformation
db.version(4)
  .stores({
    users: "++id, name, email, createdAt, profile",
    profiles: "++id, userId, bio, avatar"
  })
  .upgrade(trans => {
    // Transform email addresses to lowercase
    return trans.table("users").toCollection().modify(user => {
      user.email = user.email.toLowerCase();
    });
  });

Complex Migration Examples

Splitting Tables

// Version 1: Combined user data
db.version(1).stores({
  users: "++id, name, email, address, phone, preferences"
});

// Version 2: Split into users and user_details
db.version(2)
  .stores({
    users: "++id, name, email",
    userDetails: "++id, userId, address, phone, preferences"
  })
  .upgrade(async trans => {
    const users = await trans.table("users").toArray();
    
    // Create detail records
    const details = users.map(user => ({
      userId: user.id,
      address: user.address,
      phone: user.phone,
      preferences: user.preferences
    }));
    
    // Add details
    await trans.table("userDetails").bulkAdd(details);
    
    // Clean up users table
    await trans.table("users").toCollection().modify(user => {
      delete user.address;
      delete user.phone;
      delete user.preferences;
    });
  });

Merging Tables

// Version 1: Separate tables
db.version(1).stores({
  basicInfo: "++id, name, email",
  extendedInfo: "++id, userId, phone, address"
});

// Version 2: Merge into single table
db.version(2)
  .stores({
    basicInfo: null,      // Delete old table
    extendedInfo: null,   // Delete old table
    users: "++id, name, email, phone, address"
  })
  .upgrade(async trans => {
    const basicInfo = await trans.table("basicInfo").toArray();
    const extendedInfo = await trans.table("extendedInfo").toArray();
    
    // Create lookup map
    const extendedMap = new Map();
    extendedInfo.forEach(ext => {
      extendedMap.set(ext.userId, ext);
    });
    
    // Merge data
    const mergedUsers = basicInfo.map(basic => {
      const extended = extendedMap.get(basic.id) || {};
      return {
        id: basic.id,
        name: basic.name,
        email: basic.email,
        phone: extended.phone || null,
        address: extended.address || null
      };
    });
    
    // Add merged data
    await trans.table("users").bulkAdd(mergedUsers);
  });

Data Format Changes

// Version 1: String-based status
db.version(1).stores({
  tasks: "++id, title, status, priority"
});

// Version 2: Convert to numeric status codes
db.version(2)
  .stores({
    tasks: "++id, title, status, priority"  // Same schema, different data
  })
  .upgrade(trans => {
    const statusMap = {
      "pending": 0,
      "in_progress": 1,
      "completed": 2,
      "cancelled": 3
    };
    
    return trans.table("tasks").toCollection().modify(task => {
      task.status = statusMap[task.status] || 0;
    });
  });

// Version 3: Add new fields with defaults
db.version(3)
  .stores({
    tasks: "++id, title, status, priority, createdAt, updatedAt"
  })
  .upgrade(trans => {
    const now = Date.now();
    return trans.table("tasks").toCollection().modify(task => {
      task.createdAt = task.createdAt || now;
      task.updatedAt = now;
    });
  });

Version Management Best Practices

Sequential Versioning

// Always use sequential version numbers
db.version(1).stores({ /* initial schema */ });
db.version(2).stores({ /* updated schema */ }).upgrade(/* migration */);
db.version(3).stores({ /* further updates */ }).upgrade(/* migration */);
// Never skip version numbers or go backwards

Backward Compatibility

// Version 1: Original schema
db.version(1).stores({
  items: "++id, name, description"
});

// Version 2: Add optional field with default
db.version(2)
  .stores({
    items: "++id, name, description, category"
  })
  .upgrade(trans => {
    // Provide default values for new fields
    return trans.table("items").toCollection().modify(item => {
      item.category = item.category || "uncategorized";
    });
  });

Error Handling in Upgrades

db.version(2)
  .stores({
    users: "++id, name, email, normalizedEmail"
  })
  .upgrade(async trans => {
    try {
      await trans.table("users").toCollection().modify(user => {
        if (user.email) {
          user.normalizedEmail = user.email.toLowerCase().trim();
        }
      });
    } catch (error) {
      console.error("Migration failed:", error);
      throw error; // Re-throw to abort the upgrade
    }
  });

Schema Information Access

interface Dexie {
  /** Current database version number */
  readonly verno: number;
  /** All tables in current version */
  readonly tables: Table[];
}

interface Table {
  /** Table schema information */
  readonly schema: TableSchema;
}

interface TableSchema {
  /** Table name */
  name: string;
  /** Primary key specification */
  primKey: IndexSpec;
  /** All indexes including primary key */
  indexes: IndexSpec[];
}

Usage Examples:

// Check current version
console.log("Current database version:", db.verno);

// List all tables
console.log("Available tables:", db.tables.map(t => t.name));

// Inspect table schema
const friendsSchema = db.friends.schema;
console.log("Primary key:", friendsSchema.primKey.name);
console.log("Indexes:", friendsSchema.indexes.map(idx => idx.name));

// Check if table has specific index
const hasAgeIndex = db.friends.schema.indexes.some(idx => idx.name === "age");