Skip to content

Commit

Permalink
feat(cli): integrate D1 with "migrate diff" and "db pull" (#23662)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkomyno committed Apr 2, 2024
1 parent 4f51dba commit b8f741a
Show file tree
Hide file tree
Showing 14 changed files with 573 additions and 75 deletions.
1 change: 1 addition & 0 deletions packages/internals/src/index.ts
Expand Up @@ -56,6 +56,7 @@ export { default as byline } from './utils/byline'
export { callOnceOnSuccess } from './utils/callOnce'
export { canPrompt } from './utils/canPrompt'
export { chmodPlusX } from './utils/chmodPlusX'
export { locateLocalCloudflareD1 } from './utils/cloudflareD1'
export { drawBox } from './utils/drawBox'
export { extractPreviewFeatures } from './utils/extractPreviewFeatures'
export { formatms } from './utils/formatms'
Expand Down
47 changes: 47 additions & 0 deletions packages/internals/src/utils/cloudflareD1.ts
@@ -0,0 +1,47 @@
import path from 'node:path'
import process from 'node:process'

import glob from 'globby'
import { match } from 'ts-pattern'

const defaultD1DirPath = path.join('.wrangler', 'state', 'v3', 'd1', 'miniflare-D1DatabaseObject')

type TocateLocalCloudflareD1Args = {
arg: '--to-local-d1' | '--from-local-d1'
}

// Utility to find the location of the local Cloudflare D1 sqlite database.
// When using `wrangler`, the database is located in `${cwd}/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/<UUID>.sqlite`,
// where `<UUID>` is a unique identifier for the database.
export async function locateLocalCloudflareD1({ arg }: TocateLocalCloudflareD1Args) {
const cwd = process.cwd()
const d1DirPath = path.join(cwd, defaultD1DirPath)

const d1Databases = await glob(path.join(d1DirPath, '*.sqlite'), {})

if (d1Databases.length === 0) {
throw new Error(
`No Cloudflare D1 databases found in ${defaultD1DirPath}. Did you run \`wrangler d1 create <DATABASE_NAME>\` and \`wrangler dev\`?`,
)
}

if (d1Databases.length > 1) {
const { originalArg, recommendedArg } = match(arg)
.with('--to-local-d1', (originalArg) => ({
originalArg,
recommendedArg: '--to-url file:',
}))
.with('--from-local-d1', (originalArg) => ({
originalArg,
recommendedArg: '--from-url file:',
}))
.exhaustive()

throw new Error(
`Multiple Cloudflare D1 databases found in ${defaultD1DirPath}. Please manually specify the local D1 database with \`${recommendedArg}\`, without using the \`${originalArg}\` flag.`,
)
}

const d1Database = d1Databases[0]
return d1Database
}
Expand Up @@ -163,6 +163,36 @@ model AwesomeProfile {
`;
exports[`common/sqlite should succeed when --local-d1 and a single local Cloudflare D1 database exists 2`] = `
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = "file:.wrangler/state/v3/d1/miniflare-D1DatabaseObject/5d11bcce386042472d19a6a4f58e40041ebc5932c972e1449cbf404f3e3c4a7a.sqlite"
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
User User @relation(fields: [authorId], references: [id])
}
model User {
id Int @id @default(autoincrement())
email String @unique
count1 Int
name String?
Post Post[]
}
`;
exports[`common/sqlite should succeed when reintrospecting with --local-d1 and a single local Cloudflare D1 database exists 2`] = ``;
exports[`common/sqlite should succeed when schema is invalid and using --force 7`] = `
generator client {
provider = "prisma-client-js"
Expand Down
133 changes: 86 additions & 47 deletions packages/migrate/src/__tests__/DbPull/sqlite.test.ts
Expand Up @@ -13,6 +13,45 @@ const ctx = jestContext.new().add(jestConsoleContext()).add(jestProcessContext()
process.env.CI = 'true'

describe('common/sqlite', () => {
test('should succeed when --local-d1 and a single local Cloudflare D1 database exists', async () => {
ctx.fixture('cloudflare-d1-one-db')

const introspect = new DbPull()
const result = introspect.parse(['--local-d1', '--print'])
await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.log'].mock.calls.join('\n')).toMatchSnapshot()
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

test('should succeed when reintrospecting with --local-d1 and a single local Cloudflare D1 database exists', async () => {
ctx.fixture('re-introspection/sqlite/cloudflare-d1-one-db')

const introspect = new DbPull()
const result = introspect.parse(['--local-d1'])
await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.log'].mock.calls.join('\n')).toMatchSnapshot()
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
`)
expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting based on datasource defined in prisma/schema.prisma
✔ Introspected 2 models and wrote them into prisma/schema.prisma in XXXms
Run prisma generate to generate Prisma Client.
Without the driverAdapters preview feature, the schema introspected via the --local-d1 flag will not work with @prisma/client.
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

test('basic introspection', async () => {
ctx.fixture('introspection/sqlite')
const introspect = new DbPull()
Expand Down Expand Up @@ -96,13 +135,13 @@ describe('common/sqlite', () => {
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting based on datasource defined in schema.prisma
- Introspecting based on datasource defined in schema.prisma
✔ Introspected 3 models and wrote them into schema.prisma in XXXms
Run prisma generate to generate Prisma Client.
✔ Introspected 3 models and wrote them into schema.prisma in XXXms
Run prisma generate to generate Prisma Client.
`)
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

Expand All @@ -119,13 +158,13 @@ describe('common/sqlite', () => {
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting
- Introspecting
✔ Introspected 3 models and wrote them into schema.prisma in XXXms
Run prisma generate to generate Prisma Client.
✔ Introspected 3 models and wrote them into schema.prisma in XXXms
Run prisma generate to generate Prisma Client.
`)
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

Expand Down Expand Up @@ -159,20 +198,20 @@ describe('common/sqlite', () => {
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting based on datasource defined in prisma/reintrospection.prisma
- Introspecting based on datasource defined in prisma/reintrospection.prisma
✔ Introspected 3 models and wrote them into prisma/reintrospection.prisma in XXXms
*** WARNING ***
✔ Introspected 3 models and wrote them into prisma/reintrospection.prisma in XXXms
*** WARNING ***
These models were enriched with \`@@map\` information taken from the previous Prisma schema:
- "AwesomeNewPost"
- "AwesomeProfile"
- "AwesomeUser"
These models were enriched with \`@@map\` information taken from the previous Prisma schema:
- "AwesomeNewPost"
- "AwesomeProfile"
- "AwesomeUser"
Run prisma generate to generate Prisma Client.
Run prisma generate to generate Prisma Client.
`)
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)

expect(ctx.fs.read('prisma/reintrospection.prisma')).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -229,14 +268,14 @@ describe('common/sqlite', () => {
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(`
// *** WARNING ***
//
// These models were enriched with \`@@map\` information taken from the previous Prisma schema:
// - "AwesomeNewPost"
// - "AwesomeProfile"
// - "AwesomeUser"
//
`)
// *** WARNING ***
//
// These models were enriched with \`@@map\` information taken from the previous Prisma schema:
// - "AwesomeNewPost"
// - "AwesomeProfile"
// - "AwesomeUser"
//
`)
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)

Expand All @@ -256,13 +295,13 @@ describe('common/sqlite', () => {
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting based on datasource defined in prisma/schema.prisma
- Introspecting based on datasource defined in prisma/schema.prisma
✔ Introspected 3 models and wrote them into prisma/schema.prisma in XXXms
Run prisma generate to generate Prisma Client.
✔ Introspected 3 models and wrote them into prisma/schema.prisma in XXXms
Run prisma generate to generate Prisma Client.
`)
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

Expand Down Expand Up @@ -292,11 +331,11 @@ describe('common/sqlite', () => {
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting based on datasource defined in prisma/schema.prisma
- Introspecting based on datasource defined in prisma/schema.prisma
✖ Introspecting based on datasource defined in prisma/schema.prisma
✖ Introspecting based on datasource defined in prisma/schema.prisma
`)
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

Expand Down Expand Up @@ -327,11 +366,11 @@ describe('common/sqlite', () => {
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting based on datasource defined in prisma/schema.prisma
- Introspecting based on datasource defined in prisma/schema.prisma
✖ Introspecting based on datasource defined in prisma/schema.prisma
✖ Introspecting based on datasource defined in prisma/schema.prisma
`)
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

Expand Down Expand Up @@ -382,11 +421,11 @@ describe('common/sqlite', () => {
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting based on datasource defined in prisma/invalid.prisma
- Introspecting based on datasource defined in prisma/invalid.prisma
✖ Introspecting based on datasource defined in prisma/invalid.prisma
✖ Introspecting based on datasource defined in prisma/invalid.prisma
`)
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

Expand All @@ -405,13 +444,13 @@ describe('common/sqlite', () => {
expect(ctx.mocked['process.stdout.write'].mock.calls.join('\n')).toMatchInlineSnapshot(`
- Introspecting based on datasource defined in prisma/invalid.prisma
- Introspecting based on datasource defined in prisma/invalid.prisma
✔ Introspected 3 models and wrote them into prisma/invalid.prisma in XXXms
Run prisma generate to generate Prisma Client.
✔ Introspected 3 models and wrote them into prisma/invalid.prisma in XXXms
Run prisma generate to generate Prisma Client.
`)
`)
expect(ctx.mocked['process.stderr.write'].mock.calls.join('\n')).toMatchInlineSnapshot(``)

expect(ctx.fs.read('prisma/invalid.prisma')).toMatchSnapshot()
Expand Down

0 comments on commit b8f741a

Please sign in to comment.