bknd-modify-schema

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Modify Schema

修改Schema

Modify existing schema in Bknd: rename entities/fields, change field types, or alter constraints.
修改Bknd中的现有Schema:重命名实体/字段、变更字段类型或修改约束。

Prerequisites

前提条件

  • Existing Bknd app with entities (see
    bknd-create-entity
    )
  • For code mode: Access to
    bknd.config.ts
  • Backup your database before destructive changes
  • 已创建包含实体的Bknd应用(参考
    bknd-create-entity
  • 代码模式下:需有权限访问
    bknd.config.ts
  • 破坏性变更前请备份数据库

Critical Concept: Destructive vs Non-Destructive Changes

核心概念:破坏性变更 vs 非破坏性变更

Bknd's schema sync detects differences between your code and database. Some changes are safe; others cause data loss.
Bknd的Schema同步功能会检测代码与数据库之间的差异。部分变更安全无风险,部分则会导致数据丢失。

Non-Destructive (Safe)

非破坏性(安全)

  • Adding new entities
  • Adding new fields (nullable or with default)
  • Adding new indices
  • Loosening constraints (removing
    .required()
    )
  • 添加新实体
  • 添加新字段(可空或带有默认值)
  • 添加新索引
  • 放宽约束(移除
    .required()

Destructive (Data Loss Risk)

破坏性(存在数据丢失风险)

  • Renaming entities (treated as drop old + create new)
  • Renaming fields (treated as drop old + create new)
  • Changing field types (may fail or truncate data)
  • Removing fields (drops column and data)
  • Removing entities (drops table and all data)
  • Tightening constraints on existing data
  • 重命名实体(会被识别为删除旧实体+创建新实体)
  • 重命名字段(会被识别为删除旧字段+创建新字段)
  • 变更字段类型(可能执行失败或截断数据)
  • 删除字段(会删除列及对应数据)
  • 删除实体(会删除表及所有数据)
  • 对现有数据收紧约束

When to Use UI vs Code

UI模式与代码模式的适用场景

Use UI Mode When

适用UI模式的场景

  • Exploring schema changes interactively
  • Quick prototyping (data loss acceptable)
  • No version control needed
  • 交互式探索Schema变更
  • 快速原型开发(可接受数据丢失)
  • 无需版本控制

Use Code Mode When

适用代码模式的场景

  • Production schema changes
  • Version control required
  • Team collaboration
  • Reproducible deployments

  • 生产环境Schema变更
  • 需要版本控制
  • 团队协作
  • 可重复部署

Renaming an Entity

重命名实体

Warning: Bknd has no native rename. Renaming = DROP old + CREATE new = DATA LOSS.
警告: Bknd不支持原生重命名操作。重命名=删除旧实体+创建新实体=数据丢失

Safe Approach: Data Migration

安全方案:数据迁移

  1. Create new entity with desired name
  2. Migrate data from old to new
  3. Update code references
  4. Delete old entity
  1. 创建带有目标名称的新实体
  2. 将旧实体的数据迁移至新实体
  3. 更新代码中的引用
  4. 删除旧实体

Code Approach

代码实现方案

typescript
// Step 1: Add new entity alongside old
const schema = em({
  // OLD - will be removed later
  posts: entity("posts", {
    title: text().required(),
    content: text(),
  }),
  // NEW - desired name
  articles: entity("articles", {
    title: text().required(),
    content: text(),
  }),
});
typescript
// Step 2: Migrate data (run once via script or CLI)
const api = app.getApi();
const oldData = await api.data.readMany("posts", { limit: 10000 });

for (const item of oldData.data) {
  await api.data.createOne("articles", {
    title: item.title,
    content: item.content,
  });
}
typescript
// Step 3: Remove old entity from schema
const schema = em({
  articles: entity("articles", {
    title: text().required(),
    content: text(),
  }),
});
bash
undefined
typescript
// Step 1: Add new entity alongside old
const schema = em({
  // OLD - will be removed later
  posts: entity("posts", {
    title: text().required(),
    content: text(),
  }),
  // NEW - desired name
  articles: entity("articles", {
    title: text().required(),
    content: text(),
  }),
});
typescript
// Step 2: Migrate data (run once via script or CLI)
const api = app.getApi();
const oldData = await api.data.readMany("posts", { limit: 10000 });

for (const item of oldData.data) {
  await api.data.createOne("articles", {
    title: item.title,
    content: item.content,
  });
}
typescript
// Step 3: Remove old entity from schema
const schema = em({
  articles: entity("articles", {
    title: text().required(),
    content: text(),
  }),
});
bash
undefined

Step 4: Sync with force to drop old table

Step 4: Sync with force to drop old table

npx bknd sync --force
undefined
npx bknd sync --force
undefined

UI Approach

UI操作方案

  1. Open admin panel (
    http://localhost:1337
    )
  2. Go to Data section
  3. Create new entity with desired name
  4. Copy field definitions manually
  5. Export data from old entity (if needed)
  6. Import data to new entity
  7. Delete old entity

  1. 打开管理面板(
    http://localhost:1337
  2. 进入数据板块
  3. 创建带有目标名称的新实体
  4. 手动复制字段定义
  5. 导出旧实体的数据(如有需要)
  6. 将数据导入新实体
  7. 删除旧实体

Renaming a Field

重命名字段

Warning: Bknd treats field renames as drop + create = DATA LOSS on that column.
警告: Bknd会将字段重命名识别为删除旧字段+创建新字段=对应列的数据丢失

Safe Approach: Data Migration

安全方案:数据迁移

typescript
// Step 1: Add new field alongside old
const schema = em({
  users: entity("users", {
    name: text(),           // OLD - will be removed
    full_name: text(),      // NEW - desired name
  }),
});
typescript
// Step 2: Migrate data
const api = app.getApi();
const users = await api.data.readMany("users", { limit: 10000 });

for (const user of users.data) {
  if (user.name && !user.full_name) {
    await api.data.updateOne("users", user.id, {
      full_name: user.name,
    });
  }
}
typescript
// Step 3: Remove old field
const schema = em({
  users: entity("users", {
    full_name: text(),
  }),
});
bash
undefined
typescript
// Step 1: Add new field alongside old
const schema = em({
  users: entity("users", {
    name: text(),           // OLD - will be removed
    full_name: text(),      // NEW - desired name
  }),
});
typescript
// Step 2: Migrate data
const api = app.getApi();
const users = await api.data.readMany("users", { limit: 10000 });

for (const user of users.data) {
  if (user.name && !user.full_name) {
    await api.data.updateOne("users", user.id, {
      full_name: user.name,
    });
  }
}
typescript
// Step 3: Remove old field
const schema = em({
  users: entity("users", {
    full_name: text(),
  }),
});
bash
undefined

Step 4: Sync with force to drop old column

Step 4: Sync with force to drop old column

npx bknd sync --force
undefined
npx bknd sync --force
undefined

UI Approach

UI操作方案

  1. Add new field with desired name
  2. Write script or manually copy data
  3. Delete old field

  1. 添加带有目标名称的新字段
  2. 编写脚本或手动复制数据
  3. 删除旧字段

Changing Field Type

变更字段类型

Type changes are risky. Some conversions work; others fail or truncate.
类型变更存在风险。部分转换可正常执行,部分会失败或截断数据。

Compatible Type Changes

兼容的类型转换

FromToNotes
text
text
(with different constraints)
Usually safe
number
text
Safe (numbers become strings)
boolean
number
Safe (0/1 values)
boolean
text
Safe ("true"/"false")
原类型目标类型说明
text
text
(约束不同)
通常安全
number
text
安全(数字转为字符串)
boolean
number
安全(转换为0/1值)
boolean
text
安全(转换为"true"/"false")

Incompatible Type Changes

不兼容的类型转换

FromToRisk
text
number
Fails if non-numeric data
text
boolean
Fails if not "true"/"false"/0/1
text
date
Fails if not valid date format
json
text
May truncate; loses structure
原类型目标类型风险
text
number
若包含非数字数据则执行失败
text
boolean
若值不为"true"/"false"/0/1则执行失败
text
date
若格式不是有效日期则执行失败
json
text
可能截断数据并丢失结构

Safe Approach for Type Change

类型变更的安全方案

typescript
// Step 1: Add new field with new type
const schema = em({
  products: entity("products", {
    price: text(),            // OLD - string prices
    price_cents: number(),    // NEW - integer cents
  }),
});
typescript
// Step 2: Transform and migrate data
const api = app.getApi();
const products = await api.data.readMany("products", { limit: 10000 });

for (const product of products.data) {
  if (product.price && !product.price_cents) {
    const cents = Math.round(parseFloat(product.price) * 100);
    await api.data.updateOne("products", product.id, {
      price_cents: cents,
    });
  }
}
typescript
// Step 3: Remove old field, rename new if desired
const schema = em({
  products: entity("products", {
    price_cents: number(),
  }),
});

typescript
// Step 1: Add new field with new type
const schema = em({
  products: entity("products", {
    price: text(),            // OLD - string prices
    price_cents: number(),    // NEW - integer cents
  }),
});
typescript
// Step 2: Transform and migrate data
const api = app.getApi();
const products = await api.data.readMany("products", { limit: 10000 });

for (const product of products.data) {
  if (product.price && !product.price_cents) {
    const cents = Math.round(parseFloat(product.price) * 100);
    await api.data.updateOne("products", product.id, {
      price_cents: cents,
    });
  }
}
typescript
// Step 3: Remove old field, rename new if desired
const schema = em({
  products: entity("products", {
    price_cents: number(),
  }),
});

Changing Field Constraints

修改字段约束

Making a Field Required

设置字段为必填

Risk: Fails if existing records have null values.
typescript
// Before
entity("users", {
  email: text(),  // Optional
});

// After
entity("users", {
  email: text().required(),  // Now required
});
Safe approach:
  1. Update all null values first
  2. Then add
    .required()
typescript
// Step 1: Fill nulls with default
const api = app.getApi();
const usersWithNull = await api.data.readMany("users", {
  where: { email: { $isnull: true } },
});

for (const user of usersWithNull.data) {
  await api.data.updateOne("users", user.id, {
    email: "unknown@example.com",
  });
}

// Step 2: Now safely add .required()
风险: 若现有记录存在空值则执行失败。
typescript
// Before
entity("users", {
  email: text(),  // Optional
});

// After
entity("users", {
  email: text().required(),  // Now required
});
安全方案:
  1. 先更新所有空值
  2. 再添加
    .required()
typescript
// Step 1: Fill nulls with default
const api = app.getApi();
const usersWithNull = await api.data.readMany("users", {
  where: { email: { $isnull: true } },
});

for (const user of usersWithNull.data) {
  await api.data.updateOne("users", user.id, {
    email: "unknown@example.com",
  });
}

// Step 2: Now safely add .required()

Making a Field Unique

设置字段为唯一

Risk: Fails if duplicates exist.
typescript
// Before
entity("users", {
  username: text(),
});

// After
entity("users", {
  username: text().unique(),
});
Safe approach:
  1. Find and resolve duplicates
  2. Then add
    .unique()
typescript
// Check for duplicates via raw SQL or manual inspection
// Resolve duplicates by updating or deleting
// Then add .unique() constraint
风险: 若存在重复值则执行失败。
typescript
// Before
entity("users", {
  username: text(),
});

// After
entity("users", {
  username: text().unique(),
});
安全方案:
  1. 查找并解决重复值问题
  2. 再添加
    .unique()
typescript
// Check for duplicates via raw SQL or manual inspection
// Resolve duplicates by updating or deleting
// Then add .unique() constraint

Removing Required/Unique

移除必填/唯一约束

Generally safe:
typescript
// Before
entity("users", {
  email: text().required().unique(),
});

// After - loosening constraints is safe
entity("users", {
  email: text(),  // Now optional, non-unique
});

通常是安全的:
typescript
// Before
entity("users", {
  email: text().required().unique(),
});

// After - loosening constraints is safe
entity("users", {
  email: text(),  // Now optional, non-unique
});

The Sync Workflow

同步工作流

Preview Changes (Dry Run)

预览变更(试运行)

bash
undefined
bash
undefined

See what sync would do without applying

See what sync would do without applying

npx bknd sync

Output shows:
- New entities/fields to create
- Entities/fields to drop
- Index changes
npx bknd sync

输出内容包括:
- 待创建的新实体/字段
- 待删除的实体/字段
- 索引变更

Apply Non-Destructive Changes

应用非破坏性变更

bash
undefined
bash
undefined

Applies only additive changes

Applies only additive changes

npx bknd sync
undefined
npx bknd sync
undefined

Apply All Changes (Including Drops)

应用所有变更(包括删除操作)

bash
undefined
bash
undefined

WARNING: This will drop tables/columns

WARNING: This will drop tables/columns

npx bknd sync --force
undefined
npx bknd sync --force
undefined

Apply Drops Only

仅应用删除操作

bash
undefined
bash
undefined

Specifically enables drop operations

Specifically enables drop operations

npx bknd sync --drop

---
npx bknd sync --drop

---

UI Approach: Field Modifications

UI操作方案:字段修改

Change Field Type

变更字段类型

  1. Open entity in Data section
  2. Click on field to edit
  3. Note: Type dropdown may be locked for existing fields
  4. If locked: Create new field with correct type, migrate data, delete old
  1. 在数据板块中打开对应实体
  2. 点击字段进行编辑
  3. 注意: 现有字段的类型下拉菜单可能被锁定
  4. 若被锁定:创建带有正确类型的新字段,迁移数据后删除旧字段

Change Constraints

修改约束

  1. Open entity in Data section
  2. Click on field to edit
  3. Toggle Required/Unique as needed
  4. Click Save
  5. Click Sync Database
  1. 在数据板块中打开对应实体
  2. 点击字段进行编辑
  3. 根据需要切换必填/唯一选项
  4. 点击保存
  5. 点击同步数据库

Rename Field

重命名字段

  1. Create new field with desired name
  2. Manually copy data or write migration script
  3. Delete old field
  4. Sync database

  1. 创建带有目标名称的新字段
  2. 手动复制数据或编写迁移脚本
  3. 删除旧字段
  4. 同步数据库

Common Pitfalls

常见问题

Sync Fails on Type Change

类型变更时同步失败

Error:
Cannot convert column type from X to Y
Fix: Use migration approach - create new field, copy data, drop old.
错误:
Cannot convert column type from X to Y
解决方法: 使用迁移方案 - 创建新字段,复制数据,删除旧字段。

Sync Fails on Required Constraint

设置必填约束时同步失败

Error:
Column contains null values, cannot add NOT NULL
Fix: Update all null values to non-null first, then re-sync.
错误:
Column contains null values, cannot add NOT NULL
解决方法: 先将所有空值更新为非空值,再重新同步。

Sync Fails on Unique Constraint

设置唯一约束时同步失败

Error:
Duplicate values exist for column
Fix: Remove duplicates before adding unique constraint.
错误:
Duplicate values exist for column
解决方法: 添加唯一约束前先移除重复值。

Data Lost After Rename

重命名后数据丢失

Problem: Renamed entity/field and lost all data.
Fix: Unfortunately, data is gone. Restore from backup. Use migration approach next time.
问题: 重命名实体/字段后所有数据丢失。
解决方法: 数据已无法恢复,请从备份中恢复。下次请使用迁移方案。

Force Flag Ignored

--force参数未生效

Problem:
--force
doesn't seem to apply changes.
Fix: Check sync output for actual errors. May be validation issue, not permission.

问题:
--force
参数似乎未应用变更。
解决方法: 检查同步输出中的实际错误信息。可能是验证问题,而非权限问题。

Migration Script Template

迁移脚本模板

For complex migrations, create a standalone script:
typescript
// scripts/migrate-schema.ts
import { App } from "bknd";

async function migrate() {
  const app = new App({
    connection: { url: process.env.DB_URL! },
  });
  await app.build();

  const api = app.getApi();

  console.log("Starting migration...");

  // Read all records from old structure
  const records = await api.data.readMany("old_entity", { limit: 100000 });
  console.log(`Found ${records.data.length} records`);

  // Transform and insert into new structure
  let migrated = 0;
  for (const record of records.data) {
    await api.data.createOne("new_entity", {
      // Transform fields as needed
      new_field: record.old_field,
    });
    migrated++;
    if (migrated % 100 === 0) {
      console.log(`Migrated ${migrated}/${records.data.length}`);
    }
  }

  console.log("Migration complete!");
  process.exit(0);
}

migrate().catch(console.error);
Run with:
bash
npx bun scripts/migrate-schema.ts
对于复杂迁移,可创建独立脚本:
typescript
// scripts/migrate-schema.ts
import { App } from "bknd";

async function migrate() {
  const app = new App({
    connection: { url: process.env.DB_URL! },
  });
  await app.build();

  const api = app.getApi();

  console.log("Starting migration...");

  // Read all records from old structure
  const records = await api.data.readMany("old_entity", { limit: 100000 });
  console.log(`Found ${records.data.length} records`);

  // Transform and insert into new structure
  let migrated = 0;
  for (const record of records.data) {
    await api.data.createOne("new_entity", {
      // Transform fields as needed
      new_field: record.old_field,
    });
    migrated++;
    if (migrated % 100 === 0) {
      console.log(`Migrated ${migrated}/${records.data.length}`);
    }
  }

  console.log("Migration complete!");
  process.exit(0);
}

migrate().catch(console.error);
运行脚本:
bash
npx bun scripts/migrate-schema.ts

or

or

npx ts-node scripts/migrate-schema.ts

---
npx ts-node scripts/migrate-schema.ts

---

Verification

验证

After Schema Modification

Schema修改后验证

bash
undefined
bash
undefined

1. Check sync status

1. Check sync status

npx bknd sync
npx bknd sync

2. Verify schema in debug output

2. Verify schema in debug output

npx bknd schema --pretty
undefined
npx bknd schema --pretty
undefined

Via Code

通过代码验证

typescript
const api = app.getApi();

// Verify field exists by querying
const result = await api.data.readMany("entity_name", { limit: 1 });
console.log(result.data[0]);  // Check field names/values
typescript
const api = app.getApi();

// Verify field exists by querying
const result = await api.data.readMany("entity_name", { limit: 1 });
console.log(result.data[0]);  // Check field names/values

Via UI

通过UI验证

  1. Open entity in Data section
  2. Verify fields appear correctly
  3. Create test record with new schema
  4. Query existing records to verify data

  1. 在数据板块中打开实体
  2. 确认字段显示正确
  3. 使用新Schema创建测试记录
  4. 查询现有记录以验证数据

DOs and DON'Ts

注意事项

DO:
  • Back up database before destructive changes
  • Use migration approach for renames
  • Preview with
    npx bknd sync
    before forcing
  • Test on development database first
  • Keep old structure until data migrated
DON'T:
  • Rename entities/fields directly (data loss)
  • Use
    --force
    without previewing first
  • Change types without migration plan
  • Add
    .required()
    to fields with null data
  • Add
    .unique()
    to fields with duplicates

建议:
  • 破坏性变更前备份数据库
  • 重命名操作使用迁移方案
  • 强制执行前先用
    npx bknd sync
    预览
  • 先在开发数据库中测试
  • 数据迁移完成前保留旧结构
禁止:
  • 直接重命名实体/字段(会导致数据丢失)
  • 未预览就使用
    --force
    参数
  • 无迁移计划就变更类型
  • 为空值字段添加
    .required()
  • 为存在重复值的字段添加
    .unique()

Related Skills

相关技能

  • bknd-create-entity - Create new entities
  • bknd-add-field - Add fields to entities
  • bknd-delete-entity - Safely remove entities
  • bknd-seed-data - Populate migrated data
  • bknd-crud-update - Update records during migration
  • bknd-create-entity - 创建新实体
  • bknd-add-field - 为实体添加字段
  • bknd-delete-entity - 安全删除实体
  • bknd-seed-data - 填充迁移后的数据
  • bknd-crud-update - 迁移期间更新记录