Open DallasHoff opened 1 year ago
+1, unable to run migrate in edge runtime as well
Update: Drizzle recently released Expo support which is a great step forward for this. It supports React Native now, and with just a bit more work, it could support other types of local-first apps.
I'm still willing to help implement this, but I would need a response from a maintainer so we can coordinate.
I wrote something for crsqlite-wasm
, but I don't really know where to post it so folks find it: https://github.com/drizzle-team/drizzle-orm/issues/193#issuecomment-2028376239. It's extremely close to what would be needed for wa-sqlite
, with just a few method names changed (and how to prepare / finalize statements) so if there is interest I could write the wa-sqlite
version.
I'd love to see this added as a first class feature. Currently, I compile a json
file based on the _journal.json
file, and then import it to my migrate function. This allows me to ship my migrations with my edge-app, and it is capable of managing the migrations. You just need to remember to recompile the deployment_schema.json
file before building the application.
// compileMigrations.mjs
/* eslint-disable no-undef */
/**
* This script compiles the Drizzle migrations into a format that can be processed by a webworker
* This allows for migrations to be shipped and ran within the application
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import journal from './src/migrations/meta/_journal.json' assert { type: 'json' };
const migrate = [];
for (let index = 0; index < journal.entries.length; index++) {
const { when, idx, tag } = journal.entries[index];
console.log(`parsing ${tag}`);
const migrationFile = fs
.readFileSync(`./src/migrations/${tag}.sql`)
.toString();
migrate.push({
idx,
when,
tag,
hash: crypto.createHash('sha256').update(migrationFile).digest('hex'),
sql: migrationFile
.replace(/\n\t?/g, '')
.split('--> statement-breakpoint')
.map((x) => x.trim()),
});
}
fs.writeFileSync(
'./src/migrations/deployment_schema.json',
JSON.stringify(migrate, null, 2)
);
The code that applies the migrations is a modified version of the official D1 migrator
.
import migrations from './migrations/deployment_schema.json';
... method within a class
/**
* Run migrations using the modified journal files.
*/
migrate = async () => {
const migrationsTable = '__drizzle_migrations';
await this.drizzle.run(sql`
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
hash TEXT NOT NULL,
tag TEXT NOT NULL,
created_at numeric
)`);
const dbMigrations = await this.drizzle.values<[number, string, string]>(
sql`SELECT id, hash, created_at FROM ${sql.identifier(migrationsTable)} ORDER BY created_at DESC LIMIT 1`
);
const lastDbMigration = dbMigrations[0] ?? undefined;
const statementToBatch = [];
for (const migration of migrations) {
if (!lastDbMigration || Number(lastDbMigration[2])! < migration.when) {
for (const stmt of migration.sql) {
statementToBatch.push(
this.drizzle.run(sql.raw(stmt)) as BatchItem<'sqlite'>
);
}
statementToBatch.push(
this.drizzle.run(
sql`INSERT INTO ${sql.identifier(migrationsTable)} ("hash", "created_at", "tag") VALUES(
${sql.raw(`'${migration.hash}'`)},
${sql.raw(`${migration.when}`)},
${sql.raw(`'${migration.tag}'`)})`
) as BatchItem<'sqlite'>
);
}
}
if (statementToBatch.length > 0) {
await this.drizzle.batch(
statementToBatch as unknown as readonly [
BatchItem<'sqlite'>,
...BatchItem<'sqlite'>[],
]
);
}
};
Kinda hacky, but it seems to work just fine.
+1, this would be extremely helpful for offline-first applications
A modified version of @bruceharrison1984 solution has been working for me so far, using sql.js and drizzle-orm/sql-js.
As with Bruce's, I set up a prebuild script which deploys the migrations to a JSON config file written to the source code and then implemented a migrator that runs the migrations on the browser and keeps a record of the latest migration in the __drizzle_migrations
table of the SQLite database file.
/scripts/deploy.mjs
#!/usr/bin/env node
import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import url from 'node:url'
const { default: journal } = await import('../drizzle/meta/_journal.json', {
with: { type: 'json' }
})
const migrate = []
const root = path.resolve(url.fileURLToPath(path.dirname(import.meta.url)), '..')
const outdir = path.resolve(root, './src/migrations/')
const outfile = path.resolve(outdir, 'deployment.json')
console.log()
for (let index = 0; index < journal.entries.length; index++) {
const { when, idx, tag } = journal.entries[index]
console.log('(%d) Parsing migration tagged "%s"', index + 1, tag)
const filepath = path.resolve(root, 'drizzle', `${tag}.sql`)
const migration_file = fs.readFileSync(filepath).toString()
migrate.push({
idx,
when,
tag,
hash: crypto.createHash('sha256').update(migration_file).digest('hex'),
sql: migration_file
.replace(/\n\t?/g, '')
.split('--> statement-breakpoint')
.map((x) => x.trim())
})
}
if (fs.existsSync(outdir) === false) fs.mkdirSync(outdir)
fs.writeFileSync(outfile, JSON.stringify(migrate, null, 2))
console.log()
console.log('Migration deployment config file written out to "%s"\n', outfile)
package.json scripts
{
"postinstall": "yarn build",
"prebuild": "run-s migrate:generate migrate:deploy",
"build:tsc": "tsc",
"build": "run-s prebuild build:tsc",
"migrate:deploy": "node ./scripts/deploy.mjs",
"migrate:generate": "drizzle-kit generate"
}
migrator.ts (This runs in the browser)
import { sql } from 'drizzle-orm'
import { SQLJsDatabase } from 'drizzle-orm/sql-js'
import config from './migrations/deployment.json'
export function migrate<TSchema extends Record<string, unknown>>(db: SQLJsDatabase<TSchema>) {
const TABLE_NAME = sql.identifier('__drizzle_migrations')
db.run(
sql`
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
hash TEXT NOT NULL,
tag TEXT NOT NULL,
created_at INTEGER NOT NULL
);
`
)
const deployments = db.values<[number, string, string]>(
sql`
SELECT id,
hash,
created_at
FROM ${TABLE_NAME}
ORDER BY created_at DESC
LIMIT 1;
`
)
const deployment = deployments.at(0)
const migrations = config.filter((migration) => {
const timestamp = deployment?.at(2)
return !deployment || Number(timestamp) < migration.when
})
if (migrations.length === 0) {
return console.log('There, currently, are no migrations to deploy')
}
db.transaction((tx) => {
migrations.forEach((migration, i) => {
console.info('%d. Deploying migration:', i + 1)
console.info(' TAG => %s', migration.tag)
console.info(' HASH => %s', migration.hash)
migration.sql.forEach((stmt) => tx.run(stmt))
tx.run(
sql`
INSERT INTO ${TABLE_NAME} ("hash", "created_at", "tag") VALUES (
${sql.raw(`'${migration.hash}'`)},
${sql.raw(`${migration.when}`)},
${sql.raw(`'${migration.tag}'`)}
);
`
)
})
})
console.info('Database up to date!')
}
Describe what you want
I have been experimenting with using Drizzle to interact with an SQLite database running in the browser with WebAssembly for local-first apps. For querying in the browser, Drizzle works really well, but it is not currently possible to run migrations generated with Drizzle Kit in the browser since the
migrate
function requires Node.js packages likenode:crypto
andnode:fs
.I would like to add a method of executing Drizzle Kit migrations in the browser to Drizzle ORM. I have been prototyping such a system in this repo. Let me know what you think. The process currently works like this:
Generate the migrations with Drizzle Kit as normal.
drizzle-kit generate:sqlite --schema ./src/schema.ts --out ./src/migrations
Create an index.ts file that combines the journal file and migrations into a single file for the frontend bundle.
export const journal: MigrationJournal = _journal;
export const migrations: Record<string, string> = { '0000_broad_cardiac': Migration0000, '0001_silly_sumo': Migration0001, };
The usage of the
node:crypto
package is replaced by the Web Crypto API, and the migrations are passed directly instead of read from the filesystem in order to eliminatenode:fs
and because fetching individual migration files over the network would not be ideal.Ideally, Drizzle Kit could be configured to generate that index.ts file from step 2 or something similar that combines the journal and migrations automatically. Thoughts on this? Edit: with Expo support out, Drizzle can now almost do this, but the generated file should have a .ts extension instead of .js and the imports it contains of .sql files need to end in
?raw
to support Vite.I'd also like feedback on where in the Drizzle repo this new migrator implementation should be placed and how it should be named.