Checkout the previous post on How I deployed my Astro + Turso + Drizzle project to Cloudflare Pages if you want to know more about the project.
Background
I wanted to migrate my Turso DB to Cloudflare D1. As such this post will go into the details of the challenges I faced during the migration process.
You can find the project and the repository below:
Challenges
Making D1 work with drizzle and Astro
D1 as well as drizzle-kit requires the project to have a wrangler.toml
file. Here is mine:
#:schema node_modules/wrangler/config-schema.json
name = "map"
compatibility_date = "2024-10-11"
pages_build_output_dir = "./dist"
[placement]
mode = "smart"
[vars]
NODE_VERSION = "20.17.0"
PNPM_VERSION = "9.9.0"
[[d1_databases]]
binding = "MAP"
database_name = "map"
database_id = "10eb6d23-6724-46e8-9bf2-c4091739f69f"
migrations_dir = "migrations"
migrations_table = "__drizzle_migrations"
Ensure the corresponding D1 binding is also set in Cloudflare dashboard. Based on their docs it seemed like drizzle expects the argument to be of type D1Database
. Since env.d.ts
file is being used throughout the application for environment related stuff, I updated it like so:
/// <reference path="../.astro/types.d.ts" />
type Runtime = import("@astrojs/cloudflare").Runtime<Env>;
declare namespace App {
interface Locals extends Runtime {
env: {
CLOUDFLARE_ACCOUNT_ID: string;
CLOUDFLARE_DATABASE_ID: string;
CLOUDFLARE_D1_TOKEN: string;
CLOUDFLARE_D1_BINDING: string;
MAP: D1Database;
};
}
}
interface ImportMeta {
readonly env: {
readonly CLOUDFLARE_ACCOUNT_ID: string;
readonly CLOUDFLARE_DATABASE_ID: string;
readonly CLOUDFLARE_D1_TOKEN: string;
readonly CLOUDFLARE_D1_BINDING: string;
};
}
If you observe, MAP
in the env.d.ts
file matches the binding
in the wrangler.toml
file.
Turso dump was not working
I wanted to take a dump of my data. After searching, I found out about the dump
command. I ran turso db shell map-viz ".dump" > map-viz.sql
. map-viz
being the name of my database. However, every single time, the map-viz.sql
was being created with empty content. I'm not sure what was wrong since I was correctly executing the command and the data was also present. So I finally resorted to manually exporting the data in the CSV format.
D1 does not support importing data in the CSV format
Feeling good about myself after clearing the Turso hurdle, I was disappointed to find out that D1 does not support CSV import (at least as of this writing). So how to solve this new issue then?
I updated my env
to point to the hosted D1 and ran pnpm db:migrate
to apply the migrations to my database. Good thing was D1 allowed running SQL queries directly. Since I had the CSV data, I wanted to create SQL statements from it. What good way than to use Claude for this. I asked it to write SQL queries and provided it a couple of CSV records. After modifying the output a bit, here is the final JS file which I used:
const fs = require("fs");
const csv = require("csv-parse/sync");
const fileName = "location.csv";
const tableName = "location";
// Function to escape single quotes in string values
function escapeQuotes(value) {
return value.replace(/'/g, "''");
}
// Function to format value for SQL
function formatValue(value) {
if (value === "") return "NULL";
if (isNaN(value)) return `'${escapeQuotes(value)}'`;
return value;
}
// Read the CSV file
const fileContent = fs.readFileSync(fileName, "utf8");
// Parse the CSV content
const records = csv.parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
// Generate INSERT statements
const insertStatements = records.map((record) => {
const columns = Object.keys(record).join(", ");
const values = Object.values(record).map(formatValue).join(", ");
return `INSERT INTO ${tableName} (${columns}) VALUES (${values});`;
});
// Write INSERT statements to a file
fs.writeFileSync(`${fileName}.sql`, insertStatements.join("\n"));
console.log(`INSERT statements have been written to ${fileName}.sql`);
After running the script it generated the SQL statements like so:
INSERT INTO location (id, created_at, updated_at, latitude, longitude, city, region, region_code, country, hash, count, last_visit) VALUES (54, '2024-10-12 11:21:03', 1728732060, -34.0866, 18.8144, 'Somerset West', 'Western Cape', 'WC', 'ZA', '-34.08660-18.81440-Somerset West-Western Cape-WC-ZA-45.222.74.86', 1, 1728732060);
INSERT INTO location (id, created_at, updated_at, latitude, longitude, city, region, region_code, country, hash, count, last_visit) VALUES (64, '2024-10-12 17:19:12', 1728753549, 46.3153, 4.8344, 'Mâcon', 'Bourgogne-Franche-Comté', 'BFC', 'FR', '46.31530-4.83440-Mâcon-Bourgogne-Franche-Comté-BFC-FR-2a01:cb10:82ae:c600:259e:a34c:1e8e:a4b5', 1, 1728753549);
SQL commands now being generated, the last step was to import the data. For which I ran wrangler d1 execute map --file=./location.csv.sql --remote
and voila! The data was there in my D1 database.
Conclusion
Even though I faced some challenges during integration of Cloudflare services and migrating data, moving to D1 was a good decision since the initial load times have pretty much been cut in half.