Drizzle Seed

drizzle-seed 是一个 TypeScript 库,帮助你生成确定性但又逼真的假数据,用于填充你的数据库。通过利用可设种子的伪随机数生成器(pRNG),它确保你生成的数据在不同运行之间保持一致且可复现。这对于测试、开发和调试尤其有用。

什么是确定性数据生成?

确定性数据生成意味着相同的输入总会产生相同的输出。对于 drizzle-seed 来说,当你使用相同的种子数字初始化该库时,它每次都会生成相同的假数据序列。这使得数据集具有可预测性和可重复性。

伪随机数生成器(pRNG)

伪随机数生成器是一种算法,它生成的数字序列近似随机数的特性。然而,由于它基于一个称为种子的初始值,你可以控制它的随机性。通过使用相同的种子,pRNG 将产生相同的数字序列,从而使你的数据生成过程可复现。

使用 pRNG 的好处:

使用 drizzle-seed,你可以兼得两全其美:既能生成逼真的假数据,又能在需要时控制并复现它。

安装

npm
yarn
pnpm
bun
npm i drizzle-seed

基本用法

在这个示例中,我们将使用随机姓名和 id 创建 10 个用户

import { singlestoreTable, int, varchar } from "drizzle-orm/singlestore-core";
import { drizzle } from "drizzle-orm/singlestore";
import { seed } from "drizzle-seed";

const users = singlestoreTable("users", {
  id: int().primaryKey().autoincrement(),
  name: varchar({ length: 255 }).notNull(),
});

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);
  await seed(db, { users });
}

main();

选项

count

默认情况下,seed 函数会创建 10 个实体。 不过,如果你的测试需要更多实体,可以在 seed 选项对象中指定这一点

await seed(db, schema, { count: 1000 });

seed

如果你需要一个种子来为后续每次运行生成一组不同的值,可以在 seed 选项中定义一个不同的数字。任何新的数字都会生成一组唯一的值

await seed(db, schema, { seed: 12345 });

重置数据库

使用 drizzle-seed,你可以轻松重置数据库并用新值重新填充,例如在测试套件中

// 需要重置的 schema 文件路径
import * as schema from "./schema.ts";
import { reset } from "drizzle-seed";

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);
  await reset(db, schema);
}

main();

不同的方言会采用不同的数据库重置策略。对于 SingleStore:

对于 SingleStore,drizzle-seed 包会首先禁用 FOREIGN_KEY_CHECKS 以确保下一步不会失败,然后生成 TRUNCATE 语句来清空所有表中的内容

SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE tableName1;
TRUNCATE tableName2;
...
SET FOREIGN_KEY_CHECKS = 1;

精炼

如果你需要更改 drizzle-seed 默认使用的种子生成器函数的行为,可以指定你自己的实现,甚至可以在播种过程中使用你自己的一组值。

.refine 是一个回调,它接收来自 drizzle-seed 的所有可用生成器函数列表。它应该返回一个对象,其中的键代表你想要精炼的表,并根据需要定义它们的行为。
每个表都可以指定多个属性,以简化数据库播种过程:

info

你还可以为要创建的引用值数量指定加权随机分布。有关此 API 的详细信息,请参阅 加权随机文档 文档部分

API

await seed(db, schema).refine((f) => ({
  users: {
    columns: {},
    count: 10,
    with: {
        posts: 10
    }
  },
}));

让我们通过一些示例来看看会发生什么,并附带说明:

schema.ts
import { singlestoreTable, int, varchar } from "drizzle-orm/singlestore-core";

export const users = singlestoreTable("users", {
  id: int().primaryKey().autoincrement(),
  name: varchar({ length: 255 }).notNull(),
});

export const posts = singlestoreTable("posts", {
  id: int().primaryKey().autoincrement(),
  description: varchar({ length: 255 }),
  userId: int().references(() => users.id),
});

示例 1:仅为 users 表生成 20 条实体,并对 name 列使用精炼后的种子逻辑

index.ts
import { drizzle } from "drizzle-orm/singlestore";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, { users: schema.users }).refine((f) => ({
    users: {
        columns: {
            name: f.fullName(),
        },
        count: 20
    }
  }));
}

main();

示例 2:仅为 users 表生成数据,且将 name 列排除在播种之外

index.ts
import { drizzle } from "drizzle-orm/singlestore";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, { users: schema.users }).refine((f) => ({
    users: {
        columns: {
            name: false, // name 列不会被播种,从而允许数据库使用其默认值。
        }
    }
  }));
}

main();

示例 3:为 users 表生成 20 条实体,并通过播种 posts 表以及创建从 postsusers 的引用,为每个 user 添加 10 条 posts

index.ts
import { drizzle } from "drizzle-orm/singlestore";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, schema).refine((f) => ({
    users: {
        count: 20,
        with: {
            posts: 10
        }
    }
  }));
}

main();

示例 4:为 users 表生成 5 条实体,并向数据库填充 100 条 posts,但不将它们与 users 实体关联。为 users 精炼 id 生成逻辑,使其生成 1000020000 之间的任意整数且保持唯一,并为 posts 精炼为从自定义数组中取值

index.ts
import { drizzle } from "drizzle-orm/singlestore";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, schema).refine((f) => ({
    users: {
        count: 5,
        columns: {
            id: f.int({
              minValue: 10000,
              maxValue: 20000,
              isUnique: true,
            }),
        }
    },
    posts: {
        count: 100,
        columns: {
            description: f.valuesFromArray({
            values: [
                "太阳落在群山之后,把天空染成了橙色和紫色的层层色调", 
                "我简直不敢相信这道自制披萨居然这么好吃!", 
                "有时候,你所需要的只是一本文好书和一个安静的角落。", 
                "还有谁觉得雨天特别适合刷老电影马拉松吗?", 
                "今天尝试了一条新的徒步路线,还发现了最惊艳的瀑布!",
                // ...
            ],
          })
        }
    }
  }));
}

main();
IMPORTANT

我们将在这些文档中定义更多可能性,但目前你可以先浏览本文档中的几个部分。查看 生成器 部分,熟悉你可以使用的所有可用生成器函数。

一个特别出色的功能是支持加权随机化,无论是用于为某一列生成生成器值,还是用于确定 drizzle-seed 可以生成多少个相关实体。

请查看 加权随机文档 了解更多信息。

加权随机

有些情况下,您可能需要在 seed 阶段将多个具有不同优先级的数据集插入数据库。对于这类场景,drizzle-seed 提供了一个名为 weighted random 的 API。

Drizzle Seed 包在以下几个地方可以使用加权随机:

下面我们来看两个示例:

schema.ts
import { singlestoreTable, int, varchar, double } from "drizzle-orm/singlestore-core";

export const orders = singlestoreTable(
  "orders",
  {
    id: int().primaryKey().autoincrement(),
    name: varchar({ length: 255 }).notNull(),
    quantityPerUnit: varchar().notNull(),
    unitPrice: double().notNull(),
    unitsInStock: int().notNull(),
    unitsOnOrder: int().notNull(),
    reorderLevel: int().notNull(),
    discontinued: int().notNull(),
  }
);

export const details = singlestoreTable(
  "details",
  {
    unitPrice: double().notNull(),
    quantity: int().notNull(),
    discount: double().notNull(),

    orderId: int()
      .notNull()
      .references(() => orders.id, { onDelete: "cascade" }),
  }
);

示例 1:优化 unitPrice 的生成逻辑,生成 5000 个随机价格,其中 30% 的概率生成 10-100 之间的价格,70% 的概率生成 100-300 之间的价格

index.ts
import { drizzle } from "drizzle-orm/singlestore";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, schema).refine((f) => ({
    orders: {
       count: 5000,
       columns: {
           unitPrice: f.weightedRandom(
               [
                   {
                       weight: 0.3,
                       value: funcs.int({ minValue: 10, maxValue: 100 })
                   },
                   {
                       weight: 0.7,
                       value: funcs.number({ minValue: 100, maxValue: 300, precision: 100 })
                   }
               ]
           ),
       }
    }
  }));
}

main();

示例 2:对于每个订单,生成 1 到 3 条 details 的概率为 60%,生成 5 到 7 条 details 的概率为 30%,生成 8 到 10 条 details 的概率为 10%

index.ts
import { drizzle } from "drizzle-orm/singlestore";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, schema).refine((f) => ({
    orders: {
       with: {
           details:
               [
                   { weight: 0.6, count: [1, 2, 3] },
                   { weight: 0.3, count: [5, 6, 7] },
                   { weight: 0.1, count: [8, 9, 10] },
               ]
       }
    }
  }));
}

main();

复杂示例

main.ts
schema.ts
import { seed } from "drizzle-seed";
import * as schema from "./schema.ts";

const main = async () => {
    const titlesOfCourtesy = ["女士", "夫人", "博士"];
    const unitsOnOrders = [0, 10, 20, 30, 50, 60, 70, 80, 100];
    const reorderLevels = [0, 5, 10, 15, 20, 25, 30];
    const quantityPerUnit = [
        "100 - 100 g pieces",
        "100 - 250 g bags",
        "10 - 200 g glasses",
        "10 - 4 oz boxes",
        "10 - 500 g pkgs.",
        "10 - 500 g pkgs."
    ];
    const discounts = [0.05, 0.15, 0.2, 0.25];

    await seed(db, schema).refine((funcs) => ({
        customers: {
            count: 10000,
            columns: {
                companyName: funcs.companyName(),
                contactName: funcs.fullName(),
                contactTitle: funcs.jobTitle(),
                address: funcs.streetAddress(),
                city: funcs.city(),
                postalCode: funcs.postcode(),
                region: funcs.state(),
                country: funcs.country(),
                phone: funcs.phoneNumber({ template: "(###) ###-####" }),
                fax: funcs.phoneNumber({ template: "(###) ###-####" })
            }
        },
        employees: {
            count: 200,
            columns: {
                firstName: funcs.firstName(),
                lastName: funcs.lastName(),
                title: funcs.jobTitle(),
                titleOfCourtesy: funcs.valuesFromArray({ values: titlesOfCourtesy }),
                birthDate: funcs.date({ minDate: "2010-12-31", maxDate: "2010-12-31" }),
                hireDate: funcs.date({ minDate: "2010-12-31", maxDate: "2024-08-26" }),
                address: funcs.streetAddress(),
                city: funcs.city(),
                postalCode: funcs.postcode(),
                country: funcs.country(),
                homePhone: funcs.phoneNumber({ template: "(###) ###-####" }),
                extension: funcs.int({ minValue: 428, maxValue: 5467 }),
                notes: funcs.loremIpsum()
            }
        },
        orders: {
            count: 50000,
            columns: {
                shipVia: funcs.int({ minValue: 1, maxValue: 3 }),
                freight: funcs.number({ minValue: 0, maxValue: 1000, precision: 100 }),
                shipName: funcs.streetAddress(),
                shipCity: funcs.city(),
                shipRegion: funcs.state(),
                shipPostalCode: funcs.postcode(),
                shipCountry: funcs.country()
            },
            with: {
                details:
                    [
                        { weight: 0.6, count: [1, 2, 3, 4] },
                        { weight: 0.2, count: [5, 6, 7, 8, 9, 10] },
                        { weight: 0.15, count: [11, 12, 13, 14, 15, 16, 17] },
                        { weight: 0.05, count: [18, 19, 20, 21, 22, 23, 24, 25] },
                    ]
            }
        },
        suppliers: {
            count: 1000,
            columns: {
                companyName: funcs.companyName(),
                contactName: funcs.fullName(),
                contactTitle: funcs.jobTitle(),
                address: funcs.streetAddress(),
                city: funcs.city(),
                postalCode: funcs.postcode(),
                region: funcs.state(),
                country: funcs.country(),
                phone: funcs.phoneNumber({ template: "(###) ###-####" })
            }
        },
        products: {
            count: 5000,
            columns: {
                name: funcs.companyName(),
                quantityPerUnit: funcs.valuesFromArray({ values: quantityPerUnit }),
                unitPrice: funcs.weightedRandom(
                    [
                        {
                            weight: 0.5,
                            value: funcs.int({ minValue: 3, maxValue: 300 })
                        },
                        {
                            weight: 0.5,
                            value: funcs.number({ minValue: 3, maxValue: 300, precision: 100 })
                        }
                    ]
                ),
                unitsInStock: funcs.int({ minValue: 0, maxValue: 125 }),
                unitsOnOrder: funcs.valuesFromArray({ values: unitsOnOrders }),
                reorderLevel: funcs.valuesFromArray({ values: reorderLevels }),
                discontinued: funcs.int({ minValue: 0, maxValue: 1 })
            }
        },
        details: {
            columns: {
                unitPrice: funcs.number({ minValue: 10, maxValue: 130 }),
                quantity: funcs.int({ minValue: 1, maxValue: 130 }),
                discount: funcs.weightedRandom(
                    [
                        { weight: 0.5, value: funcs.valuesFromArray({ values: discounts }) },
                        { weight: 0.5, value: funcs.default({ defaultValue: 0 }) }
                    ]
                )
            }
        }
    }));
}

main();

限制

with 的类型限制

由于某些 TypeScript 限制以及 Drizzle 当前的 API,无法正确推断表之间的引用关系,尤其是在表之间存在循环依赖时。

这意味着 with 选项会显示 schema 中的所有表,你需要手动选择其中具有一对多关系的表

warning

with 选项适用于一对多关系。例如,如果你有一个 user 和多个 posts,你可以在 users 上使用 with 包含 posts,但不能在 posts 上使用 with 包含 users

Drizzle 表第三个参数的类型限制:

目前,我们不支持 Drizzle 表第三个参数的类型。虽然它在运行时可以工作,但在类型层面不会正常生效