diff --git a/.github/workflows/router.yaml b/.github/workflows/router.yaml index 0505bc9282..3da3eb1219 100644 --- a/.github/workflows/router.yaml +++ b/.github/workflows/router.yaml @@ -8,12 +8,49 @@ on: workflow_dispatch: jobs: + switch: + runs-on: ubuntu-24.04 + outputs: + target: ${{ steps.route.outputs.target }} + steps: + - name: Route release + id: route + shell: bash + run: | + HEAD_REPO="${{ github.event.pull_request.head.repo.full_name }}" + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "${GITHUB_REF##*/}" == "main" ]]; then + echo "target=latest" >> $GITHUB_OUTPUT + # only run on all pushes or pull requests from forks + elif [[ "$GITHUB_EVENT_NAME" == "push" ]] || [[ "$HEAD_REPO" != "$GITHUB_REPOSITORY" ]]; then + echo "target=feature" >> $GITHUB_OUTPUT + else + echo "target=skip" >> $GITHUB_OUTPUT + fi + run-feature: - if: github.event_name != 'workflow_dispatch' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) + needs: switch + if: needs.switch.outputs.target == 'feature' uses: ./.github/workflows/release-feature-branch.yaml - secrets: inherit + secrets: + PLANETSCALE_CONNECTION_STRING: ${{ secrets.PLANETSCALE_CONNECTION_STRING }} + NEON_CONNECTION_STRING: ${{ secrets.NEON_CONNECTION_STRING }} + # NEON_HTTP_CONNECTION_STRING: ${{ secrets.NEON_CONNECTION_STRING }} + TIDB_CONNECTION_STRING: ${{ secrets.TIDB_CONNECTION_STRING }} + XATA_API_KEY: ${{ secrets.XATA_API_KEY }} + XATA_BRANCH: ${{ secrets.XATA_BRANCH }} + LIBSQL_REMOTE_URL: ${{ secrets.LIBSQL_REMOTE_URL }} + LIBSQL_REMOTE_TOKEN: ${{ secrets.LIBSQL_REMOTE_TOKEN }} run-latest: - if: github.event_name == 'workflow_dispatch' && github.ref_name == 'main' + needs: switch + if: needs.switch.outputs.target == 'latest' uses: ./.github/workflows/release-latest.yaml - secrets: inherit \ No newline at end of file + secrets: + PLANETSCALE_CONNECTION_STRING: ${{ secrets.PLANETSCALE_CONNECTION_STRING }} + NEON_CONNECTION_STRING: ${{ secrets.NEON_CONNECTION_STRING }} + # NEON_HTTP_CONNECTION_STRING: ${{ secrets.NEON_CONNECTION_STRING }} + TIDB_CONNECTION_STRING: ${{ secrets.TIDB_CONNECTION_STRING }} + XATA_API_KEY: ${{ secrets.XATA_API_KEY }} + XATA_BRANCH: ${{ secrets.XATA_BRANCH }} + LIBSQL_REMOTE_URL: ${{ secrets.LIBSQL_REMOTE_URL }} + LIBSQL_REMOTE_TOKEN: ${{ secrets.LIBSQL_REMOTE_TOKEN }} \ No newline at end of file diff --git a/changelogs/drizzle-kit/0.31.9.md b/changelogs/drizzle-kit/0.31.9.md new file mode 100644 index 0000000000..3bdb169da8 --- /dev/null +++ b/changelogs/drizzle-kit/0.31.9.md @@ -0,0 +1 @@ +- drizzle-kit api improvements for D1 connections \ No newline at end of file diff --git a/drizzle-kit/package.json b/drizzle-kit/package.json index b5af4ac75a..093ac1d517 100644 --- a/drizzle-kit/package.json +++ b/drizzle-kit/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-kit", - "version": "0.31.8", + "version": "0.31.9", "homepage": "https://orm.drizzle.team", "keywords": [ "drizzle", diff --git a/drizzle-kit/src/api.ts b/drizzle-kit/src/api.ts index 83e593dbd6..336fce5f60 100644 --- a/drizzle-kit/src/api.ts +++ b/drizzle-kit/src/api.ts @@ -1,3 +1,4 @@ +/// import type { PGlite } from '@electric-sql/pglite'; import { randomUUID } from 'crypto'; import { is } from 'drizzle-orm'; @@ -327,7 +328,10 @@ export const pushSQLiteSchema = async ( export const startStudioSQLiteServer = async ( imports: Record, - credentials: SqliteCredentials, + credentials: SqliteCredentials | { + driver: 'd1'; + binding: D1Database; + }, options?: { host?: string; port?: number; diff --git a/drizzle-kit/src/cli/connections.ts b/drizzle-kit/src/cli/connections.ts index 2c653c138d..d79a07e010 100644 --- a/drizzle-kit/src/cli/connections.ts +++ b/drizzle-kit/src/cli/connections.ts @@ -1,3 +1,4 @@ +/// import type { PGlite } from '@electric-sql/pglite'; import type { AwsDataApiPgQueryResult, AwsDataApiSessionOptions } from 'drizzle-orm/aws-data-api/pg'; import type { MigrationConfig } from 'drizzle-orm/migrator'; @@ -890,7 +891,7 @@ const prepareSqliteParams = (params: any[], driver?: string) => { ? JSON.stringify(param.value) : (param.value as string); - if (driver === 'd1-http') { + if (driver === 'd1-http' || driver === 'd1') { return value; } @@ -919,6 +920,81 @@ const preparePGliteParams = (params: any[]) => { }); }; +export type D1Credentials = { + driver: 'd1'; + binding: D1Database; +}; + +export const connectToD1 = async ( + d1: D1Database, +): Promise< + & SQLiteDB + & { + packageName: 'd1'; + migrate: (config: MigrationConfig) => Promise; + proxy: Proxy; + transactionProxy: TransactionProxy; + } +> => { + const db: SQLiteDB = { + query: async (sql: string, params?: any[]) => { + const stmt = d1.prepare(sql); + const boundStmt = params && params.length > 0 ? stmt.bind(...params) : stmt; + const result = await boundStmt.all(); + return (result.results ?? []) as T[]; + }, + run: async (query: string) => { + const stmt = d1.prepare(query); + await stmt.run(); + }, + }; + + const proxy: Proxy = async (params) => { + const preparedParams = prepareSqliteParams(params.params || [], 'd1'); + const stmt = d1.prepare(params.sql); + const boundStmt = preparedParams.length > 0 ? stmt.bind(...preparedParams) : stmt; + + try { + if (params.mode === 'array') { + return await boundStmt.raw(); + } + const result = await boundStmt.all(); + return result.results ?? []; + } catch (error: any) { + // D1 doesn't allow certain introspection queries (sqlite_master with pragma functions) + // Return empty array for SQLITE_AUTH errors on these system queries + if (error?.message?.includes('SQLITE_AUTH') || error?.message?.includes('not authorized')) { + return []; + } + throw error; + } + }; + + const transactionProxy: TransactionProxy = async (queries) => { + const results: any[] = []; + try { + // D1 doesn't support true transactions via binding, use batch instead + const statements = queries.map((q) => d1.prepare(q.sql)); + const batchResults = await d1.batch(statements); + for (const result of batchResults) { + results.push(result.results ?? []); + } + } catch (error) { + results.push(error as Error); + } + return results; + }; + + const { drizzle } = await import('drizzle-orm/d1'); + const { migrate } = await import('drizzle-orm/d1/migrator'); + const drzl = drizzle(d1); + const migrateFn = async (config: MigrationConfig) => { + return migrate(drzl, config); + }; + + return { ...db, packageName: 'd1', proxy, transactionProxy, migrate: migrateFn }; +}; + export const connectToSQLite = async ( credentials: SqliteCredentials, ): Promise< diff --git a/drizzle-kit/src/serializer/studio.ts b/drizzle-kit/src/serializer/studio.ts index 246d80f55c..35d3623328 100644 --- a/drizzle-kit/src/serializer/studio.ts +++ b/drizzle-kit/src/serializer/studio.ts @@ -1,3 +1,4 @@ +/// import type { PGlite } from '@electric-sql/pglite'; import { serve } from '@hono/node-server'; import { zValidator } from '@hono/zod-validator'; @@ -64,9 +65,10 @@ export type Setup = { | 'mysql2' | '@planetscale/database' | 'd1-http' + | 'd1' | '@libsql/client' | 'better-sqlite3'; - driver?: 'aws-data-api' | 'd1-http' | 'turso' | 'pglite'; + driver?: 'aws-data-api' | 'd1-http' | 'd1' | 'turso' | 'pglite'; databaseName?: string; // for planetscale (driver remove database name from connection string) proxy: Proxy; transactionProxy: TransactionProxy; @@ -360,17 +362,45 @@ export const drizzleForMySQL = async ( }; }; +// D1 binding credentials type (mirrors the one in connections.ts) +type D1BindingCredentials = { + driver: 'd1'; + binding: D1Database; +}; + export const drizzleForSQLite = async ( - credentials: SqliteCredentials, + credentials: SqliteCredentials | D1BindingCredentials, sqliteSchema: Record>, relations: Record, schemaFiles?: SchemaFile[], casing?: CasingType, ): Promise => { - const { connectToSQLite } = await import('../cli/connections'); + const customDefaults = getCustomDefaults(sqliteSchema, casing); + if ('driver' in credentials && credentials.driver === 'd1') { + const { connectToD1 } = await import('../cli/connections'); + const sqliteDB = await connectToD1(credentials.binding); + + const dbUrl = 'd1://binding'; + const dbHash = createHash('sha256').update(dbUrl).digest('hex'); + + return { + dbHash, + dialect: 'sqlite', + driver: 'd1', + packageName: 'd1', + proxy: sqliteDB.proxy, + transactionProxy: sqliteDB.transactionProxy, + customDefaults, + schema: sqliteSchema, + relations, + schemaFiles, + casing, + }; + } + + const { connectToSQLite } = await import('../cli/connections'); const sqliteDB = await connectToSQLite(credentials); - const customDefaults = getCustomDefaults(sqliteSchema, casing); let dbUrl: string;