From 7d0085c9b8d4a221c1611ed761e241a8bfd1420e Mon Sep 17 00:00:00 2001 From: Maston <22679886+mastondzn@users.noreply.github.com> Date: Sat, 20 May 2023 08:54:43 +0300 Subject: [PATCH 01/10] Make migrators accept databases drizzle'd with schemas Fixes case where you specify your schemas when you use `drizzle()`, and `migrate()` wont accept the database type --- drizzle-orm/src/aws-data-api/pg/migrator.ts | 4 +++- drizzle-orm/src/better-sqlite3/migrator.ts | 6 ++++-- drizzle-orm/src/bun-sqlite/migrator.ts | 7 +++++-- drizzle-orm/src/d1/migrator.ts | 6 ++++-- drizzle-orm/src/libsql/migrator.ts | 5 ++++- drizzle-orm/src/mysql2/migrator.ts | 4 +++- drizzle-orm/src/neon-serverless/migrator.ts | 7 +++++-- drizzle-orm/src/node-postgres/migrator.ts | 6 ++++-- drizzle-orm/src/planetscale-serverless/migrator.ts | 4 +++- drizzle-orm/src/postgres-js/migrator.ts | 6 ++++-- drizzle-orm/src/sql-js/migrator.ts | 7 +++++-- drizzle-orm/src/sqlite-proxy/migrator.ts | 9 +++++++-- drizzle-orm/src/vercel-postgres/migrator.ts | 6 ++++-- 13 files changed, 55 insertions(+), 22 deletions(-) diff --git a/drizzle-orm/src/aws-data-api/pg/migrator.ts b/drizzle-orm/src/aws-data-api/pg/migrator.ts index 44bbf83fe..e2c66bb42 100644 --- a/drizzle-orm/src/aws-data-api/pg/migrator.ts +++ b/drizzle-orm/src/aws-data-api/pg/migrator.ts @@ -2,7 +2,9 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { AwsDataApiPgDatabase } from './driver'; -export async function migrate(db: AwsDataApiPgDatabase, config: string | MigrationConfig) { +export async function migrate< + T extends AwsDataApiPgDatabase>, +>(db: T, config: string | MigrationConfig) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/better-sqlite3/migrator.ts b/drizzle-orm/src/better-sqlite3/migrator.ts index 2b3574a6b..c5d85659a 100644 --- a/drizzle-orm/src/better-sqlite3/migrator.ts +++ b/drizzle-orm/src/better-sqlite3/migrator.ts @@ -1,8 +1,10 @@ -import type { MigrationConfig} from '~/migrator'; +import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { BetterSQLite3Database } from './driver'; -export function migrate(db: BetterSQLite3Database, config: string | MigrationConfig) { +export function migrate< + T extends BetterSQLite3Database>, +>(db: T, config: string | MigrationConfig) { const migrations = readMigrationFiles(config); db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/bun-sqlite/migrator.ts b/drizzle-orm/src/bun-sqlite/migrator.ts index 22839fb03..ac4e8856b 100644 --- a/drizzle-orm/src/bun-sqlite/migrator.ts +++ b/drizzle-orm/src/bun-sqlite/migrator.ts @@ -1,8 +1,11 @@ -import type { MigrationConfig} from '~/migrator'; +import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { BunSQLiteDatabase } from './driver'; -export function migrate(db: BunSQLiteDatabase, config: string | MigrationConfig) { +export function migrate>>( + db: T, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/d1/migrator.ts b/drizzle-orm/src/d1/migrator.ts index 2fa427da5..675e212e3 100644 --- a/drizzle-orm/src/d1/migrator.ts +++ b/drizzle-orm/src/d1/migrator.ts @@ -1,8 +1,10 @@ -import type { MigrationConfig} from '~/migrator'; +import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { DrizzleD1Database } from './driver'; -export async function migrate(db: DrizzleD1Database, config: string | MigrationConfig) { +export async function migrate< + T extends DrizzleD1Database>, +>(db: T, config: string | MigrationConfig) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/libsql/migrator.ts b/drizzle-orm/src/libsql/migrator.ts index a35c1ccbb..41d681f3d 100644 --- a/drizzle-orm/src/libsql/migrator.ts +++ b/drizzle-orm/src/libsql/migrator.ts @@ -2,7 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { LibSQLDatabase } from './driver'; -export function migrate(db: LibSQLDatabase, config: MigrationConfig) { +export function migrate>>( + db: T, + config: MigrationConfig, +) { const migrations = readMigrationFiles(config); return db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/mysql2/migrator.ts b/drizzle-orm/src/mysql2/migrator.ts index 5f1dfb262..e4fa7ff69 100644 --- a/drizzle-orm/src/mysql2/migrator.ts +++ b/drizzle-orm/src/mysql2/migrator.ts @@ -2,7 +2,9 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { MySql2Database } from './driver'; -export async function migrate(db: MySql2Database, config: MigrationConfig) { +export async function migrate< + T extends MySql2Database>, +>(db: T, config: MigrationConfig) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session, config); } diff --git a/drizzle-orm/src/neon-serverless/migrator.ts b/drizzle-orm/src/neon-serverless/migrator.ts index 0b273b71d..30dbfba3f 100644 --- a/drizzle-orm/src/neon-serverless/migrator.ts +++ b/drizzle-orm/src/neon-serverless/migrator.ts @@ -1,8 +1,11 @@ -import type { MigrationConfig} from '~/migrator'; +import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { NeonDatabase } from './driver'; -export async function migrate(db: NeonDatabase, config: string | MigrationConfig) { +export async function migrate>>( + db: T, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/node-postgres/migrator.ts b/drizzle-orm/src/node-postgres/migrator.ts index a6aad72c3..cfbefb47e 100644 --- a/drizzle-orm/src/node-postgres/migrator.ts +++ b/drizzle-orm/src/node-postgres/migrator.ts @@ -1,8 +1,10 @@ -import type { MigrationConfig} from '~/migrator'; +import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { NodePgDatabase } from './driver'; -export async function migrate(db: NodePgDatabase, config: string | MigrationConfig) { +export async function migrate< + T extends NodePgDatabase>, +>(db: T, config: string | MigrationConfig) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/planetscale-serverless/migrator.ts b/drizzle-orm/src/planetscale-serverless/migrator.ts index d0a0a5168..417a5a1fc 100644 --- a/drizzle-orm/src/planetscale-serverless/migrator.ts +++ b/drizzle-orm/src/planetscale-serverless/migrator.ts @@ -2,7 +2,9 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { PlanetScaleDatabase } from './driver'; -export async function migrate(db: PlanetScaleDatabase, config: MigrationConfig) { +export async function migrate< + T extends PlanetScaleDatabase>, +>(db: T, config: MigrationConfig) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session, config); } diff --git a/drizzle-orm/src/postgres-js/migrator.ts b/drizzle-orm/src/postgres-js/migrator.ts index 7baea52f5..09b528444 100644 --- a/drizzle-orm/src/postgres-js/migrator.ts +++ b/drizzle-orm/src/postgres-js/migrator.ts @@ -1,8 +1,10 @@ -import type { MigrationConfig} from '~/migrator'; +import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { PostgresJsDatabase } from './driver'; -export async function migrate(db: PostgresJsDatabase, config: string | MigrationConfig) { +export async function migrate< + T extends PostgresJsDatabase>, +>(db: T, config: string | MigrationConfig) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/sql-js/migrator.ts b/drizzle-orm/src/sql-js/migrator.ts index c192c16a6..f858e9d0f 100644 --- a/drizzle-orm/src/sql-js/migrator.ts +++ b/drizzle-orm/src/sql-js/migrator.ts @@ -1,8 +1,11 @@ -import type { MigrationConfig} from '~/migrator'; +import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { SQLJsDatabase } from './driver'; -export function migrate(db: SQLJsDatabase, config: string | MigrationConfig) { +export function migrate>>( + db: T, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/sqlite-proxy/migrator.ts b/drizzle-orm/src/sqlite-proxy/migrator.ts index 7bbdb0ea8..7620d46f5 100644 --- a/drizzle-orm/src/sqlite-proxy/migrator.ts +++ b/drizzle-orm/src/sqlite-proxy/migrator.ts @@ -5,7 +5,9 @@ import type { SqliteRemoteDatabase } from './driver'; export type ProxyMigrator = (migrationQueries: string[]) => Promise; -export async function migrate(db: SqliteRemoteDatabase, callback: ProxyMigrator, config: string | MigrationConfig) { +export async function migrate< + T extends SqliteRemoteDatabase>, +>(db: T, callback: ProxyMigrator, config: string | MigrationConfig) { const migrations = readMigrationFiles(config); const migrationTableCreate = sql` @@ -26,7 +28,10 @@ export async function migrate(db: SqliteRemoteDatabase, callback: ProxyMigrator, const queriesToRun: string[] = []; for (const migration of migrations) { - if (!lastDbMigration || Number(lastDbMigration[2])! < migration.folderMillis) { + if ( + !lastDbMigration + || Number(lastDbMigration[2])! < migration.folderMillis + ) { queriesToRun.push( ...migration.sql, `INSERT INTO "__drizzle_migrations" ("hash", "created_at") VALUES('${migration.hash}', '${migration.folderMillis}')`, diff --git a/drizzle-orm/src/vercel-postgres/migrator.ts b/drizzle-orm/src/vercel-postgres/migrator.ts index b7d2c8e50..7d86e82ff 100644 --- a/drizzle-orm/src/vercel-postgres/migrator.ts +++ b/drizzle-orm/src/vercel-postgres/migrator.ts @@ -1,8 +1,10 @@ -import type { MigrationConfig} from '~/migrator'; +import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { VercelPgDatabase } from './driver'; -export async function migrate(db: VercelPgDatabase, config: string | MigrationConfig) { +export async function migrate< + T extends VercelPgDatabase>, +>(db: T, config: string | MigrationConfig) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } From da9936bca3bd86c5bb63b226e7bef5ae2d1ce9eb Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Sat, 20 May 2023 20:55:03 +0300 Subject: [PATCH 02/10] Add test case from GH https://github.com/drizzle-team/drizzle-orm/issues/599 --- .../issues-schemas/mysql.duplicates.test.ts | 276 ++++++++++++++++++ .../issues-schemas/mysql.duplicates.ts | 111 +++++++ integration-tests/vitest.config.ts | 2 +- 3 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 integration-tests/tests/relational/issues-schemas/mysql.duplicates.test.ts create mode 100644 integration-tests/tests/relational/issues-schemas/mysql.duplicates.ts diff --git a/integration-tests/tests/relational/issues-schemas/mysql.duplicates.test.ts b/integration-tests/tests/relational/issues-schemas/mysql.duplicates.test.ts new file mode 100644 index 000000000..f2d44bf33 --- /dev/null +++ b/integration-tests/tests/relational/issues-schemas/mysql.duplicates.test.ts @@ -0,0 +1,276 @@ +import 'dotenv/config'; +import Docker from 'dockerode'; +import { sql } from 'drizzle-orm'; +import { drizzle, type MySql2Database } from 'drizzle-orm/mysql2'; +import getPort from 'get-port'; +import * as mysql from 'mysql2/promise'; +import { v4 as uuid } from 'uuid'; +import { afterAll, beforeAll, beforeEach, expect, expectTypeOf, test } from 'vitest'; +import * as schema from './mysql.duplicates'; + +const ENABLE_LOGGING = false; + +/* + Test cases: + - querying nested relation without PK with additional fields +*/ + +let mysqlContainer: Docker.Container; +let db: MySql2Database; +let client: mysql.Connection; + +async function createDockerDB(): Promise { + const docker = new Docker(); + const port = await getPort({ port: 3306 }); + const image = 'mysql:8'; + + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve(err))) + ); + + mysqlContainer = await docker.createContainer({ + Image: image, + Env: ['MYSQL_ROOT_PASSWORD=mysql', 'MYSQL_DATABASE=drizzle'], + name: `drizzle-integration-tests-${uuid()}`, + HostConfig: { + AutoRemove: true, + PortBindings: { + '3306/tcp': [{ HostPort: `${port}` }], + }, + }, + }); + + await mysqlContainer.start(); + + return `mysql://root:mysql@127.0.0.1:${port}/drizzle`; +} + +beforeAll(async () => { + const connectionString = process.env['MYSQL_CONNECTION_STRING'] ?? await createDockerDB(); + + const sleep = 1000; + let timeLeft = 30000; + let connected = false; + let lastError: unknown | undefined; + do { + try { + client = await mysql.createConnection(connectionString); + await client.connect(); + connected = true; + break; + } catch (e) { + lastError = e; + await new Promise((resolve) => setTimeout(resolve, sleep)); + timeLeft -= sleep; + } + } while (timeLeft > 0); + if (!connected) { + console.error('Cannot connect to MySQL'); + await client?.end().catch(console.error); + await mysqlContainer?.stop().catch(console.error); + throw lastError; + } + db = drizzle(client, { schema, logger: ENABLE_LOGGING }); +}); + +afterAll(async () => { + await client?.end().catch(console.error); + await mysqlContainer?.stop().catch(console.error); +}); + +beforeEach(async () => { + await db.execute(sql`drop table if exists \`members\``); + await db.execute(sql`drop table if exists \`artist_to_member\``); + await db.execute(sql`drop table if exists \`artists\``); + await db.execute(sql`drop table if exists \`albums\``); + + await db.execute( + sql` + CREATE TABLE \`members\` ( + \`id\` serial AUTO_INCREMENT PRIMARY KEY NOT NULL, + \`created_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updated_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`name_en\` varchar(50) NOT NULL, + \`name_kr\` varchar(50) NOT NULL, + \`stage_name_en\` varchar(50) NOT NULL, + \`stage_name_kr\` varchar(50) NOT NULL, + \`image\` varchar(255) NOT NULL, + \`instagram\` varchar(255) NOT NULL); + `, + ); + await db.execute( + sql` + CREATE TABLE \`artist_to_member\` ( + \`id\` serial AUTO_INCREMENT PRIMARY KEY NOT NULL, + \`member_id\` int NOT NULL, + \`artist_id\` int NOT NULL); + `, + ); + await db.execute( + sql` + CREATE TABLE \`artists\` ( + \`id\` serial AUTO_INCREMENT PRIMARY KEY NOT NULL, + \`created_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updated_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`name_en\` varchar(50) NOT NULL, + \`name_kr\` varchar(50) NOT NULL, + \`debut\` date NOT NULL, + \`company_id\` int NOT NULL, + \`is_group\` boolean NOT NULL DEFAULT true, + \`image\` varchar(255) NOT NULL, + \`twitter\` varchar(255) NOT NULL, + \`instagram\` varchar(255) NOT NULL, + \`youtube\` varchar(255) NOT NULL, + \`website\` varchar(255) NOT NULL, + \`spotify_id\` varchar(32)); + `, + ); + await db.execute( + sql` + CREATE TABLE \`albums\` ( + \`id\` serial AUTO_INCREMENT PRIMARY KEY NOT NULL, + \`created_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updated_at\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`artist_id\` int NOT NULL, + \`name\` varchar(50) NOT NULL, + \`region\` enum('en','kr','jp','other') NOT NULL, + \`release_date\` date NOT NULL, + \`image\` varchar(255) NOT NULL, + \`spotify_id\` varchar(32)); + `, + ); +}); + +test('Simple case from GH', async () => { + await db.insert(schema.artists).values([ + { + id: 1, + nameEn: 'Dan', + nameKr: '', + debut: new Date(), + companyId: 1, + image: '', + twitter: '', + instagram: '', + youtube: '', + website: '', + }, + { + id: 2, + nameEn: 'Andrew', + nameKr: '', + debut: new Date(), + companyId: 1, + image: '', + twitter: '', + instagram: '', + youtube: '', + website: '', + }, + { + id: 3, + nameEn: 'Alex', + nameKr: '', + debut: new Date(), + companyId: 1, + image: '', + twitter: '', + instagram: '', + youtube: '', + website: '', + }, + ]); + + await db.insert(schema.albums).values([ + { id: 1, artistId: 1, name: 'Album1', region: 'en', releaseDate: new Date(), image: '' }, + { id: 2, artistId: 2, name: 'Album2', region: 'en', releaseDate: new Date(), image: '' }, + { id: 3, artistId: 3, name: 'Album3', region: 'en', releaseDate: new Date(), image: '' }, + ]); + + await db.insert(schema.members).values([ + { id: 1, nameEn: 'MemberA', nameKr: '', stageNameEn: '', stageNameKr: '', image: '', instagram: '' }, + { id: 2, nameEn: 'MemberB', nameKr: '', stageNameEn: '', stageNameKr: '', image: '', instagram: '' }, + { id: 3, nameEn: 'MemberC', nameKr: '', stageNameEn: '', stageNameKr: '', image: '', instagram: '' }, + ]); + + await db.insert(schema.artistsToMembers).values([ + { memberId: 1, artistId: 1 }, + { memberId: 2, artistId: 1 }, + { memberId: 2, artistId: 2 }, + { memberId: 3, artistId: 3 }, + ]); + + const response = await db.query.artists.findFirst({ + where: (artists, { eq }) => eq(artists.id, 1), + with: { + albums: true, + members: { + columns: {}, + with: { + member: true, + }, + }, + }, + }); + + console.log(JSON.stringify(response, null, 2)); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + createdAt: Date; + updatedAt: Date; + nameEn: string; + nameKr: string; + debut: Date; + companyId: number; + isGroup: boolean; + image: string; + twitter: string; + instagram: string; + youtube: string; + website: string; + spotifyId: string | null; + members: { + member: { + id: number; + createdAt: Date; + updatedAt: Date; + nameEn: string; + nameKr: string; + image: string; + instagram: string; + stageNameEn: string; + stageNameKr: string; + }; + }[]; + albums: { + id: number; + name: string; + createdAt: Date; + updatedAt: Date; + image: string; + spotifyId: string | null; + artistId: number; + region: 'en' | 'kr' | 'jp' | 'other'; + releaseDate: Date; + }[]; + } | undefined + >(); + + expect(response?.members.length).eq(2); + expect(response?.albums.length).eq(1); + + expect(response?.albums[0]).toEqual({ + id: 1, + createdAt: response?.albums[0]?.createdAt, + updatedAt: response?.albums[0]?.updatedAt, + artistId: 1, + name: 'Album1', + region: 'en', + releaseDate: response?.albums[0]?.releaseDate, + image: '', + spotifyId: null, + }); +}); diff --git a/integration-tests/tests/relational/issues-schemas/mysql.duplicates.ts b/integration-tests/tests/relational/issues-schemas/mysql.duplicates.ts new file mode 100644 index 000000000..2f58f9ab3 --- /dev/null +++ b/integration-tests/tests/relational/issues-schemas/mysql.duplicates.ts @@ -0,0 +1,111 @@ +import { relations, sql } from "drizzle-orm"; +import { boolean, date, index, int, mysqlEnum, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core"; + +export const artists = mysqlTable( + 'artists', + { + id: serial('id').primaryKey(), + createdAt: timestamp('created_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: timestamp('updated_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + nameEn: varchar('name_en', { length: 50 }).notNull(), + nameKr: varchar('name_kr', { length: 50 }).notNull(), + debut: date('debut').notNull(), + companyId: int('company_id').notNull(), + isGroup: boolean('is_group').notNull().default(true), + image: varchar('image', { length: 255 }).notNull(), + twitter: varchar('twitter', { length: 255 }).notNull(), + instagram: varchar('instagram', { length: 255 }).notNull(), + youtube: varchar('youtube', { length: 255 }).notNull(), + website: varchar('website', { length: 255 }).notNull(), + spotifyId: varchar('spotify_id', { length: 32 }), + }, + (table) => ({ + nameEnIndex: index('artists__name_en__idx').on(table.nameEn), + }), +); + +export const members = mysqlTable('members', { + id: serial('id').primaryKey(), + createdAt: timestamp('created_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: timestamp('updated_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + nameEn: varchar('name_en', { length: 50 }).notNull(), + nameKr: varchar('name_kr', { length: 50 }).notNull(), + stageNameEn: varchar('stage_name_en', { length: 50 }).notNull(), + stageNameKr: varchar('stage_name_kr', { length: 50 }).notNull(), + image: varchar('image', { length: 255 }).notNull(), + instagram: varchar('instagram', { length: 255 }).notNull(), +}); + +export const artistsToMembers = mysqlTable( + 'artist_to_member', + { + id: serial('id').primaryKey(), + memberId: int('member_id').notNull(), + artistId: int('artist_id').notNull(), + }, + (table) => ({ + memberArtistIndex: index('artist_to_member__artist_id__member_id__idx').on( + table.memberId, + table.artistId, + ), + }), +); + +export const albums = mysqlTable( + 'albums', + { + id: serial('id').primaryKey(), + createdAt: timestamp('created_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: timestamp('updated_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + artistId: int('artist_id').notNull(), + name: varchar('name', { length: 50 }).notNull(), + region: mysqlEnum('region', ['en', 'kr', 'jp', 'other']).notNull(), + releaseDate: date('release_date').notNull(), + image: varchar('image', { length: 255 }).notNull(), + spotifyId: varchar('spotify_id', { length: 32 }), + }, + (table) => ({ + artistIndex: index('albums__artist_id__idx').on(table.artistId), + nameIndex: index('albums__name__idx').on(table.name), + }), +); + +// relations +export const artistRelations = relations(artists, ({ many }) => ({ + albums: many(albums), + members: many(artistsToMembers), +})); + +export const albumRelations = relations(albums, ({ one }) => ({ + artist: one(artists, { + fields: [albums.artistId], + references: [artists.id], + }), +})); + +export const memberRelations = relations(members, ({ many }) => ({ + artists: many(artistsToMembers), +})); + +export const artistsToMembersRelations = relations(artistsToMembers, ({ one }) => ({ + artist: one(artists, { + fields: [artistsToMembers.artistId], + references: [artists.id], + }), + member: one(members, { + fields: [artistsToMembers.memberId], + references: [members.id], + }), +})); diff --git a/integration-tests/vitest.config.ts b/integration-tests/vitest.config.ts index 7f7018e09..a897b87be 100644 --- a/integration-tests/vitest.config.ts +++ b/integration-tests/vitest.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ['tests/relational/*.test.ts'], + include: ['tests/relational/**/*.test.ts'], typecheck: { tsconfig: 'tsconfig.json', }, From 2a386e8f4793dbf510af8d8c1f0fb55544fbd740 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Mon, 22 May 2023 02:12:53 +0300 Subject: [PATCH 03/10] Fix including multiple relations on the same level --- README.md | 9 +- drizzle-orm/src/better-sqlite3/session.ts | 2 - drizzle-orm/src/mysql-core/dialect.ts | 151 +++++++------- .../src/mysql-core/query-builders/query.ts | 3 +- drizzle-orm/src/pg-core/dialect.ts | 192 ++++++++++-------- .../src/pg-core/query-builders/query.ts | 3 +- drizzle-orm/src/relations.ts | 2 +- drizzle-orm/src/sqlite-core/dialect.ts | 147 +++++++------- .../src/sqlite-core/query-builders/query.ts | 3 +- .../tests/relational/bettersqlite.test.ts | 4 +- integration-tests/tests/relational/pg.test.ts | 3 - package.json | 2 +- pnpm-lock.yaml | 44 ++-- 13 files changed, 286 insertions(+), 279 deletions(-) diff --git a/README.md b/README.md index 6aaa2d951..6cd78120f 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,18 @@

Drizzle ORM npm

npm npm bundle size -Discord +Discord License
If you know SQL, you know Drizzle ORM

Drizzle ORM is a TypeScript ORM for SQL databases designed with maximum type safety in mind. It comes with a [drizzle-kit](https://github.com/drizzle-team/drizzle-kit-mirror) CLI companion for automatic SQL migrations generation. Drizzle ORM is meant to be a library, not a framework. It stays as an opt-in solution all the time at any levels. - -The ORM main philosophy is "If you know SQL, you know Drizzle ORM". We follow the SQL-like syntax whenever possible, are strongly typed ground up and fail at compile time, not in runtime. +The ORM's main philosophy is "If you know SQL, you know Drizzle ORM". We follow the SQL-like syntax whenever possible, are strongly typed ground up, and fail at compile time, not in runtime. Drizzle ORM is being battle-tested on production projects by multiple teams 🚀 Give it a try and let us know if you have any questions or feedback on [Discord](https://discord.gg/yfjTbVXMW4). -## Feature list +## Features - Full type safety - [Smart automated migrations generation](https://github.com/drizzle-team/drizzle-kit-mirror) @@ -28,7 +27,7 @@ Drizzle ORM is being battle-tested on production projects by multiple teams 🚀 ## Documentation -Check the full documenation on [the website](https://orm.drizzle.team) +Check the full documentation on [the website](https://orm.drizzle.team) ## Supported databases diff --git a/drizzle-orm/src/better-sqlite3/session.ts b/drizzle-orm/src/better-sqlite3/session.ts index 6353bc094..fc9a2c0d3 100644 --- a/drizzle-orm/src/better-sqlite3/session.ts +++ b/drizzle-orm/src/better-sqlite3/session.ts @@ -1,5 +1,4 @@ import type { Database, RunResult, Statement } from 'better-sqlite3'; -import util from 'node:util'; import type { Logger } from '~/logger'; import { NoopLogger } from '~/logger'; import { type RelationalSchemaConfig, type TablesRelationalConfig } from '~/relations'; @@ -101,7 +100,6 @@ export class PreparedQuery const rows = this.values(placeholderValues); if (customResultMapper) { - console.log('rows:', util.inspect(rows, { depth: null, colors: true })); return customResultMapper(rows) as T['all']; } return rows.map((row) => mapResultRow(fields!, row, joinsNotNullableMap)); diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 86b9be523..861434145 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -397,18 +397,7 @@ export class MySqlDialect { return { tableTsKey: tableConfig.tsName, - sql: this.buildSelectQuery({ - table, - fields: {}, - fieldsFlat: selectionEntries.map(([, c]) => ({ - path: [c.name], - field: c as AnyMySqlColumn, - })), - groupBy: [], - orderBy: [], - joins: [], - withList: [], - }), + sql: table, selection, }; } @@ -480,10 +469,36 @@ export class MySqlDialect { fieldsSelection[key] = value; } + let where; + if (config.where) { + const whereSql = typeof config.where === 'function' ? config.where(aliasedFields, operators) : config.where; + where = whereSql && mapColumnsInSQLToAlias(whereSql, tableAlias); + } + + const groupBy = (tableConfig.primaryKey.length ? tableConfig.primaryKey : Object.values(tableConfig.columns)).map( + (c) => aliasedTableColumn(c as AnyMySqlColumn, tableAlias), + ); + + let orderByOrig = typeof config.orderBy === 'function' + ? config.orderBy(aliasedFields, orderByOperators) + : config.orderBy ?? []; + if (!Array.isArray(orderByOrig)) { + orderByOrig = [orderByOrig]; + } + const orderBy = orderByOrig.map((orderByValue) => { + if (orderByValue instanceof Column) { + return aliasedTableColumn(orderByValue, tableAlias) as AnyMySqlColumn; + } + return mapColumnsInSQLToAlias(orderByValue, tableAlias); + }); + const builtRelations: { key: string; value: BuildRelationalQueryResult }[] = []; const joins: JoinsValue[] = []; const builtRelationFields: SelectedFieldsOrdered = []; + let result; + + let selectedRelationIndex = 0; for (const { key: selectedRelationKey, value: selectedRelationValue } of selectedRelations) { let relation: Relation | undefined; for (const [relationKey, relationValue] of Object.entries(tableConfig.relations)) { @@ -513,9 +528,20 @@ export class MySqlDialect { ); builtRelations.push({ key: selectedRelationKey, value: builtRelation }); - joins.push({ - table: new Subquery(builtRelation.sql, {}, relationAlias), - alias: selectedRelationKey, + let relationWhere; + if (typeof selectedRelationValue === 'object' && selectedRelationValue.limit) { + const field = sql`${sql.identifier(relationAlias)}.${sql.identifier('__drizzle_row_number')}`; + relationWhere = and( + relationWhere, + or(and(sql`${field} <= ${selectedRelationValue.limit}`), sql`(${field} is null)`), + ); + } + + const join: JoinsValue = { + table: builtRelation.sql instanceof Table + ? aliasedTable(builtRelation.sql as AnyMySqlTable, relationAlias) + : new Subquery(builtRelation.sql, {}, relationAlias), + alias: relationAlias, on: and( ...normalizedRelation.fields.map((field, i) => eq( @@ -525,7 +551,7 @@ export class MySqlDialect { ), ), joinType: 'left', - }); + }; const elseField = sql`json_arrayagg(json_array(${ sql.join( @@ -541,10 +567,41 @@ export class MySqlDialect { sql.join(normalizedRelation.references.map((c) => aliasedTableColumn(c, relationAlias)), sql.raw(' or ')) }) = 0, '[]', ${elseField})`.as(selectedRelationKey); - builtRelationFields.push({ + const builtRelationField = { path: [selectedRelationKey], field, + }; + + result = this.buildSelectQuery({ + table: result ? new Subquery(result, {}, tableAlias) : aliasedTable(table, tableAlias), + fields: {}, + fieldsFlat: [ + ...Object.entries(tableConfig.columns).map(([tsKey, column]) => ({ + path: [tsKey], + field: aliasedTableColumn(column, tableAlias) as AnyMySqlColumn, + })), + ...(selectedRelationIndex === selectedRelations.length - 1 + ? selectedExtras.map(({ key, value }) => ({ + path: [key], + field: value, + })) + : []), + ...builtRelationFields.map(({ path, field }) => ({ + path, + field: sql`${sql.identifier(tableAlias)}.${sql.identifier((field as SQL.Aliased).fieldAlias)}`, + })), + builtRelationField, + ], + where: relationWhere, + groupBy, + orderBy: selectedRelationIndex === selectedRelations.length - 1 ? orderBy : [], + joins: [join], + withList: [], }); + + joins.push(join); + builtRelationFields.push(builtRelationField); + selectedRelationIndex++; } const finalFieldsSelection: SelectedFieldsOrdered = Object.entries(fieldsSelection).map(([key, value]) => { @@ -554,24 +611,6 @@ export class MySqlDialect { }; }); - const initialWhere = and( - ...selectedRelations.filter(({ key }) => { - const relation = config.with?.[key]; - return typeof relation === 'object' && (relation as DBQueryConfig<'many'>).limit !== undefined; - }).map(({ key }) => { - const field = sql`${sql.identifier(`${tableAlias}_${key}`)}.${sql.identifier('__drizzle_row_number')}`; - const value = config.with![key] as DBQueryConfig<'many'>; - const cond = or(and(sql`${field} <= ${value.limit}`), sql`(${field} is null)`); - return cond; - }), - ); - - const groupBy = (builtRelationFields.length - ? (tableConfig.primaryKey.length ? tableConfig.primaryKey : Object.values(tableConfig.columns)).map((c) => - aliasedTableColumn(c, tableAlias) - ) - : []) as AnyMySqlColumn[]; - const finalFieldsFlat: SelectedFieldsOrdered = isRoot ? [ ...finalFieldsSelection.map(({ path, field }) => ({ @@ -605,30 +644,6 @@ export class MySqlDialect { }); } - const initialFieldsFlat: SelectedFieldsOrdered = [ - { - path: [], - field: sql`${sql.identifier(tableAlias)}.*`, - }, - ...selectedExtras.map(({ key, value }) => ({ - path: [key], - field: value, - })), - ...builtRelationFields, - ]; - - let orderByOrig = typeof config.orderBy === 'function' - ? config.orderBy(aliasedFields, orderByOperators) - : config.orderBy ?? []; - if (!Array.isArray(orderByOrig)) { - orderByOrig = [orderByOrig]; - } - const orderBy = orderByOrig.map((orderByValue) => { - if (orderByValue instanceof Column) { - return aliasedTableColumn(orderByValue, tableAlias) as AnyMySqlColumn; - } - return mapColumnsInSQLToAlias(orderByValue, tableAlias); - }); if (!isRoot && !config.limit && orderBy.length > 0) { finalFieldsFlat.push({ path: ['__drizzle_row_number'], @@ -653,24 +668,8 @@ export class MySqlDialect { } } - let result = this.buildSelectQuery({ - table: aliasedTable(table, tableAlias), - fields: {}, - fieldsFlat: initialFieldsFlat, - where: initialWhere, - groupBy, - orderBy: [], - joins, - withList: [], - }); - - let where; - if (config.where) { - const whereSql = typeof config.where === 'function' ? config.where(aliasedFields, operators) : config.where; - where = whereSql && mapColumnsInSQLToAlias(whereSql, tableAlias); - } result = this.buildSelectQuery({ - table: new Subquery(result, {}, tableAlias), + table: result ? new Subquery(result, {}, tableAlias) : aliasedTable(table, tableAlias), fields: {}, fieldsFlat: finalFieldsFlat, where, diff --git a/drizzle-orm/src/mysql-core/query-builders/query.ts b/drizzle-orm/src/mysql-core/query-builders/query.ts index 65da3e5ce..8ea771259 100644 --- a/drizzle-orm/src/mysql-core/query-builders/query.ts +++ b/drizzle-orm/src/mysql-core/query-builders/query.ts @@ -6,6 +6,7 @@ import { type TableRelationalConfig, type TablesRelationalConfig, } from '~/relations'; +import { type SQL } from '~/sql'; import { type KnownKeysOnly } from '~/utils'; import { type MySqlDialect } from '../dialect'; import { @@ -97,7 +98,7 @@ export class MySqlRelationalQuery< true, ); - const builtQuery = this.dialect.sqlToQuery(query.sql); + const builtQuery = this.dialect.sqlToQuery(query.sql as SQL); return this.session.prepareQuery( builtQuery, undefined, diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index f52570877..d4b07fd90 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -425,19 +425,7 @@ export class PgDialect { return { tableTsKey: tableConfig.tsName, - sql: this.buildSelectQuery({ - table, - fields: {}, - fieldsFlat: selectionEntries.map(([, c]) => ({ - path: [c.name], - field: c as AnyPgColumn, - })), - groupBy: [], - orderBy: [], - joins: [], - lockingClauses: [], - withList: [], - }), + sql: table, selection, }; } @@ -509,10 +497,38 @@ export class PgDialect { fieldsSelection[key] = value; } + let where; + if (config.where) { + const whereSql = typeof config.where === 'function' ? config.where(aliasedFields, operators) : config.where; + where = whereSql && mapColumnsInSQLToAlias(whereSql, tableAlias); + } + + const groupBy = ((tableConfig.primaryKey.length > 0 && selectedRelations.length < 2) + ? tableConfig.primaryKey + : Object.values(tableConfig.columns)).map( + (c) => aliasedTableColumn(c as AnyPgColumn, tableAlias), + ); + + let orderByOrig = typeof config.orderBy === 'function' + ? config.orderBy(aliasedFields, orderByOperators) + : config.orderBy ?? []; + if (!Array.isArray(orderByOrig)) { + orderByOrig = [orderByOrig]; + } + const orderBy = orderByOrig.map((orderByValue) => { + if (orderByValue instanceof Column) { + return aliasedTableColumn(orderByValue, tableAlias) as AnyPgColumn; + } + return mapColumnsInSQLToAlias(orderByValue, tableAlias); + }); + const builtRelations: { key: string; value: BuildRelationalQueryResult }[] = []; const joins: JoinsValue[] = []; const builtRelationFields: SelectedFieldsOrdered = []; + let result; + + let selectedRelationIndex = 0; for (const { key: selectedRelationKey, value: selectedRelationValue } of selectedRelations) { let relation: Relation | undefined; for (const [relationKey, relationValue] of Object.entries(tableConfig.relations)) { @@ -544,9 +560,20 @@ export class PgDialect { ); builtRelations.push({ key: selectedRelationKey, value: builtRelation }); - joins.push({ - table: new Subquery(builtRelation.sql, {}, relationAlias), - alias: selectedRelationKey, + let relationWhere; + if (typeof selectedRelationValue === 'object' && selectedRelationValue.limit) { + const field = sql`${sql.identifier(relationAlias)}.${sql.identifier('__drizzle_row_number')}`; + relationWhere = and( + relationWhere, + or(and(sql`${field} <= ${selectedRelationValue.limit}`), sql`(${field} is null)`), + ); + } + + const join: JoinsValue = { + table: builtRelation.sql instanceof Table + ? aliasedTable(builtRelation.sql as AnyPgTable, relationAlias) + : new Subquery(builtRelation.sql, {}, relationAlias), + alias: relationAlias, on: and( ...normalizedRelation.fields.map((field, i) => eq( @@ -556,7 +583,7 @@ export class PgDialect { ), ), joinType: 'left', - }); + }; const relationAliasedColumns = Object.fromEntries( Object.entries(relationConfig.columns).map(([key, value]) => [key, aliasedTableColumn(value, tableAlias)]), @@ -568,7 +595,7 @@ export class PgDialect { const relationAliasedFields = Object.assign({}, relationAliasedColumns, relationAliasedRelations); - let orderBy: (SQL | AnyPgColumn)[] | undefined; + let relationOrderBy: (SQL | AnyPgColumn)[] | undefined; if (typeof selectedRelationValue === 'object') { let orderByOrig = typeof selectedRelationValue.orderBy === 'function' ? selectedRelationValue.orderBy(relationAliasedFields, orderByOperators) @@ -576,7 +603,7 @@ export class PgDialect { if (!Array.isArray(orderByOrig)) { orderByOrig = [orderByOrig]; } - orderBy = orderByOrig.map((orderByValue) => { + relationOrderBy = orderByOrig.map((orderByValue) => { if (orderByValue instanceof Column) { return aliasedTableColumn(orderByValue, relationAlias) as AnyPgColumn; } @@ -584,7 +611,9 @@ export class PgDialect { }); } - const orderBySql = orderBy?.length ? sql` order by ${sql.join(orderBy, sql`, `)}` : undefined; + const relationOrderBySql = relationOrderBy?.length + ? sql` order by ${sql.join(relationOrderBy, sql`, `)}` + : undefined; const elseField = sql`json_agg(json_build_array(${ sql.join( @@ -594,16 +623,53 @@ export class PgDialect { }), sql`, `, ) - })${orderBySql})`; + })${relationOrderBySql})`; + + if (selectedRelations.length > 1) { + elseField.append(sql.raw('::text')); + } const field = sql`case when count(${ sql.join(normalizedRelation.references.map((c) => aliasedTableColumn(c, relationAlias)), sql.raw(' or ')) }) = 0 then '[]' else ${elseField} end`.as(selectedRelationKey); - builtRelationFields.push({ + const builtRelationField = { path: [selectedRelationKey], field, + }; + + result = this.buildSelectQuery({ + table: result ? new Subquery(result, {}, tableAlias) : aliasedTable(table, tableAlias), + fields: {}, + fieldsFlat: [ + { + path: [], + field: sql`${sql.identifier(tableAlias)}.*`, + }, + ...(selectedRelationIndex === selectedRelations.length - 1 + ? selectedExtras.map(({ key, value }) => ({ + path: [key], + field: value, + })) + : []), + builtRelationField, + ], + where: relationWhere, + groupBy: [ + ...groupBy, + ...builtRelationFields.map(({ field }) => + sql`${sql.identifier(tableAlias)}.${sql.identifier((field as SQL.Aliased).fieldAlias)}` + ), + ], + orderBy: selectedRelationIndex === selectedRelations.length - 1 ? orderBy : [], + joins: [join], + withList: [], + lockingClauses: [], }); + + joins.push(join); + builtRelationFields.push(builtRelationField); + selectedRelationIndex++; } const finalFieldsSelection: SelectedFieldsOrdered = Object.entries(fieldsSelection).map(([key, value]) => { @@ -613,37 +679,6 @@ export class PgDialect { }; }); - const initialWhere = and( - ...selectedRelations.filter(({ key }) => { - const relation = config.with?.[key]; - return typeof relation === 'object' && (relation as DBQueryConfig<'many'>).limit !== undefined; - }).map(({ key }) => { - const field = sql`${sql.identifier(`${tableAlias}_${key}`)}.${sql.identifier('__drizzle_row_number')}`; - const value = config.with?.[key] as DBQueryConfig<'many'>; - const cond = or(and(sql`${field} <= ${value.limit}`), sql`(${field} is null)`); - return cond; - }), - ); - - const groupBy = (builtRelationFields.length - ? (tableConfig.primaryKey.length ? tableConfig.primaryKey : Object.values(tableConfig.columns)).map((c) => - aliasedTableColumn(c, tableAlias) - ) - : []) as AnyPgColumn[]; - - let orderByOrig = typeof config.orderBy === 'function' - ? config.orderBy(aliasedFields, orderByOperators) - : config.orderBy ?? []; - if (!Array.isArray(orderByOrig)) { - orderByOrig = [orderByOrig]; - } - const orderBy = orderByOrig.map((orderByValue) => { - if (orderByValue instanceof Column) { - return aliasedTableColumn(orderByValue, tableAlias) as AnyPgColumn; - } - return mapColumnsInSQLToAlias(orderByValue, tableAlias); - }); - const finalFieldsFlat: SelectedFieldsOrdered = isRoot ? [ ...finalFieldsSelection.map(({ path, field }) => ({ @@ -652,25 +687,23 @@ export class PgDialect { })), ...builtRelationFields.map(({ path, field }) => ({ path, - field: sql`${sql.identifier((field as SQL.Aliased).fieldAlias)}`, + field: sql`${sql.identifier((field as SQL.Aliased).fieldAlias)}${ + selectedRelations.length > 1 ? sql.raw('::json') : undefined + }`, })), ] - : [{ - path: [], - field: sql`${sql.identifier(tableAlias)}.*`, - }]; - - const initialFieldsFlat: SelectedFieldsOrdered = [ - { - path: [], - field: sql`${sql.identifier(tableAlias)}.*`, - }, - ...selectedExtras.map(({ key, value }) => ({ - path: [key], - field: value, - })), - ...builtRelationFields, - ]; + : [ + { + path: [], + field: sql`${sql.identifier(tableAlias)}.*`, + }, + ...(builtRelationFields.length === 0 + ? selectedExtras.map(({ key, value }) => ({ + path: [key], + field: value, + })) + : []), + ]; let limit, offset; @@ -689,25 +722,8 @@ export class PgDialect { } } - let result = this.buildSelectQuery({ - table: aliasedTable(table, tableAlias), - fields: {}, - fieldsFlat: initialFieldsFlat, - where: initialWhere, - groupBy, - orderBy: [], - joins, - lockingClauses: [], - withList: [], - }); - - let where; - if (config.where) { - const whereSql = typeof config.where === 'function' ? config.where(aliasedFields, operators) : config.where; - where = whereSql && mapColumnsInSQLToAlias(whereSql, tableAlias); - } result = this.buildSelectQuery({ - table: new Subquery(result, {}, tableAlias), + table: result ? new Subquery(result, {}, tableAlias) : aliasedTable(table, tableAlias), fields: {}, fieldsFlat: finalFieldsFlat, where, diff --git a/drizzle-orm/src/pg-core/query-builders/query.ts b/drizzle-orm/src/pg-core/query-builders/query.ts index 0826e7687..7e83a7688 100644 --- a/drizzle-orm/src/pg-core/query-builders/query.ts +++ b/drizzle-orm/src/pg-core/query-builders/query.ts @@ -6,6 +6,7 @@ import { type TableRelationalConfig, type TablesRelationalConfig, } from '~/relations'; +import { type SQL } from '~/sql'; import { type KnownKeysOnly } from '~/utils'; import { type PgDialect } from '../dialect'; import { type PgSession, type PreparedQuery, type PreparedQueryConfig } from '../session'; @@ -85,7 +86,7 @@ export class PgRelationalQuery extends QueryPromise { true, ); - const builtQuery = this.dialect.sqlToQuery(query.sql); + const builtQuery = this.dialect.sqlToQuery(query.sql as SQL); return this.session.prepareQuery( builtQuery, undefined, diff --git a/drizzle-orm/src/relations.ts b/drizzle-orm/src/relations.ts index 5ab88ab2b..df4322358 100644 --- a/drizzle-orm/src/relations.ts +++ b/drizzle-orm/src/relations.ts @@ -485,7 +485,7 @@ export interface BuildRelationalQueryResult { isJson: boolean; selection: BuildRelationalQueryResult['selection']; }[]; - sql: SQL; + sql: Table | SQL; } export function mapRelationalRow( diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 15cf0fa36..2ddb7bf1f 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -338,18 +338,7 @@ export abstract class SQLiteDialect { return { tableTsKey: tableConfig.tsName, - sql: this.buildSelectQuery({ - table, - fields: {}, - fieldsFlat: selectionEntries.map(([, c]) => ({ - path: [c.name], - field: c as AnySQLiteColumn, - })), - groupBy: [], - orderBy: [], - joins: [], - withList: [], - }), + sql: table, selection, }; } @@ -421,10 +410,36 @@ export abstract class SQLiteDialect { fieldsSelection[key] = value; } + let where; + if (config.where) { + const whereSql = typeof config.where === 'function' ? config.where(aliasedFields, operators) : config.where; + where = whereSql && mapColumnsInSQLToAlias(whereSql, tableAlias); + } + + const groupBy = (tableConfig.primaryKey.length ? tableConfig.primaryKey : Object.values(tableConfig.columns)).map( + (c) => aliasedTableColumn(c as AnySQLiteColumn, tableAlias), + ); + + let orderByOrig = typeof config.orderBy === 'function' + ? config.orderBy(aliasedFields, orderByOperators) + : config.orderBy ?? []; + if (!Array.isArray(orderByOrig)) { + orderByOrig = [orderByOrig]; + } + const orderBy = orderByOrig.map((orderByValue) => { + if (orderByValue instanceof Column) { + return aliasedTableColumn(orderByValue, tableAlias) as AnySQLiteColumn; + } + return mapColumnsInSQLToAlias(orderByValue, tableAlias); + }); + const builtRelations: { key: string; value: BuildRelationalQueryResult }[] = []; const joins: JoinsValue[] = []; const builtRelationFields: SelectedFieldsOrdered = []; + let result; + + let selectedRelationIndex = 0; for (const { key: selectedRelationKey, value: selectedRelationValue } of selectedRelations) { let relation: Relation | undefined; for (const [relationKey, relationValue] of Object.entries(tableConfig.relations)) { @@ -454,9 +469,20 @@ export abstract class SQLiteDialect { ); builtRelations.push({ key: selectedRelationKey, value: builtRelation }); - joins.push({ - table: new Subquery(builtRelation.sql, {}, relationAlias), - alias: selectedRelationKey, + let relationWhere; + if (typeof selectedRelationValue === 'object' && selectedRelationValue.limit) { + const field = sql`${sql.identifier(relationAlias)}.${sql.identifier('__drizzle_row_number')}`; + relationWhere = and( + relationWhere, + or(and(sql`${field} <= ${selectedRelationValue.limit}`), sql`(${field} is null)`), + ); + } + + const join: JoinsValue = { + table: builtRelation.sql instanceof Table + ? aliasedTable(builtRelation.sql as AnySQLiteTable, relationAlias) + : new Subquery(builtRelation.sql, {}, relationAlias), + alias: relationAlias, on: and( ...normalizedRelation.fields.map((field, i) => eq( @@ -466,7 +492,7 @@ export abstract class SQLiteDialect { ), ), joinType: 'left', - }); + }; const elseField = sql`json_group_array(json_array(${ sql.join( @@ -482,10 +508,37 @@ export abstract class SQLiteDialect { sql.join(normalizedRelation.references.map((c) => aliasedTableColumn(c, relationAlias)), sql.raw(' or ')) }) = 0 then '[]' else ${elseField} end`.as(selectedRelationKey); - builtRelationFields.push({ + const builtRelationField = { path: [selectedRelationKey], field, + }; + + result = this.buildSelectQuery({ + table: result ? new Subquery(result, {}, tableAlias) : aliasedTable(table, tableAlias), + fields: {}, + fieldsFlat: [ + { + path: [], + field: sql`${sql.identifier(tableAlias)}.*`, + }, + ...(selectedRelationIndex === selectedRelations.length - 1 + ? selectedExtras.map(({ key, value }) => ({ + path: [key], + field: value, + })) + : []), + builtRelationField, + ], + where: relationWhere, + groupBy, + orderBy: selectedRelationIndex === selectedRelations.length - 1 ? orderBy : [], + joins: [join], + withList: [], }); + + joins.push(join); + builtRelationFields.push(builtRelationField); + selectedRelationIndex++; } const finalFieldsSelection: SelectedFieldsOrdered = Object.entries(fieldsSelection).map(([key, value]) => { @@ -495,24 +548,6 @@ export abstract class SQLiteDialect { }; }); - const initialWhere = and( - ...selectedRelations.filter(({ key }) => { - const relation = config.with?.[key]; - return typeof relation === 'object' && (relation as DBQueryConfig<'many'>).limit !== undefined; - }).map(({ key }) => { - const field = sql`${sql.identifier(`${tableAlias}_${key}`)}.${sql.identifier('__drizzle_row_number')}`; - const value = config.with![key] as DBQueryConfig<'many'>; - const cond = or(and(sql`${field} <= ${value.limit}`), sql`(${field} is null)`); - return cond; - }), - ); - - const groupBy = (builtRelationFields.length - ? (tableConfig.primaryKey.length ? tableConfig.primaryKey : Object.values(tableConfig.columns)).map((c) => - aliasedTableColumn(c, tableAlias) - ) - : []) as AnySQLiteColumn[]; - const finalFieldsFlat: SelectedFieldsOrdered = isRoot ? [ ...finalFieldsSelection.map(({ path, field }) => ({ @@ -546,30 +581,6 @@ export abstract class SQLiteDialect { }); } - const initialFieldsFlat: SelectedFieldsOrdered = [ - { - path: [], - field: sql`${sql.identifier(tableAlias)}.*`, - }, - ...selectedExtras.map(({ key, value }) => ({ - path: [key], - field: value, - })), - ...builtRelationFields, - ]; - - let orderByOrig = typeof config.orderBy === 'function' - ? config.orderBy(aliasedFields, orderByOperators) - : config.orderBy ?? []; - if (!Array.isArray(orderByOrig)) { - orderByOrig = [orderByOrig]; - } - const orderBy = orderByOrig.map((orderByValue) => { - if (orderByValue instanceof Column) { - return aliasedTableColumn(orderByValue, tableAlias) as AnySQLiteColumn; - } - return mapColumnsInSQLToAlias(orderByValue, tableAlias); - }); if (!isRoot && !config.limit && orderBy.length > 0) { finalFieldsFlat.push({ path: ['__drizzle_row_number'], @@ -594,24 +605,8 @@ export abstract class SQLiteDialect { } } - let result = this.buildSelectQuery({ - table: aliasedTable(table, tableAlias), - fields: {}, - fieldsFlat: initialFieldsFlat, - where: initialWhere, - groupBy, - orderBy: [], - joins, - withList: [], - }); - - let where; - if (config.where) { - const whereSql = typeof config.where === 'function' ? config.where(aliasedFields, operators) : config.where; - where = whereSql && mapColumnsInSQLToAlias(whereSql, tableAlias); - } result = this.buildSelectQuery({ - table: new Subquery(result, {}, tableAlias), + table: result ? new Subquery(result, {}, tableAlias) : aliasedTable(table, tableAlias), fields: {}, fieldsFlat: finalFieldsFlat, where, diff --git a/drizzle-orm/src/sqlite-core/query-builders/query.ts b/drizzle-orm/src/sqlite-core/query-builders/query.ts index 168204332..d49216421 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/query.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/query.ts @@ -6,6 +6,7 @@ import { type TableRelationalConfig, type TablesRelationalConfig, } from '~/relations'; +import { type SQL } from '~/sql'; import { applyMixins, type KnownKeysOnly } from '~/utils'; import { type SQLiteDialect } from '../dialect'; import { type PreparedQuery, type PreparedQueryConfig, type Result, type SQLiteSession } from '../session'; @@ -169,7 +170,7 @@ export class SQLiteRelationalQuery { name: 'Group2', description: null, }, - },{ + }, { group: { id: 3, name: 'Group3', @@ -5788,7 +5788,7 @@ test('Get users with groups + custom', async () => { response[0]?.usersToGroups.sort((a, b) => (a.group.id > b.group.id) ? 1 : -1); response[1]?.usersToGroups.sort((a, b) => (a.group.id > b.group.id) ? 1 : -1); response[2]?.usersToGroups.sort((a, b) => (a.group.id > b.group.id) ? 1 : -1); - + expect(response.length).toEqual(3); expect(response[0]?.usersToGroups.length).toEqual(1); diff --git a/integration-tests/tests/relational/pg.test.ts b/integration-tests/tests/relational/pg.test.ts index 8a5a6d52b..3bddd4dd2 100644 --- a/integration-tests/tests/relational/pg.test.ts +++ b/integration-tests/tests/relational/pg.test.ts @@ -74,7 +74,6 @@ beforeAll(async () => { client = new Client(connectionString); await client.connect(); connected = true; - console.log('connected'); break; } catch (e) { lastError = e; @@ -92,10 +91,8 @@ beforeAll(async () => { }); afterAll(async () => { - console.log('deleting'); await client?.end().catch(console.error); await pgContainer?.stop().catch(console.error); - console.log('deleted'); }); beforeEach(async (ctx) => { diff --git a/package.json b/package.json index 74fd12eef..e998435ab 100755 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "eslint-plugin-unused-imports": "^2.0.0", "prettier": "^2.8.7", "resolve-tspaths": "^0.8.8", - "turbo": "^1.9.3", + "turbo": "^1.9.8", "typescript": "5.0.3" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cec0613b..332aed279 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ importers: specifier: ^0.8.8 version: 0.8.8(typescript@5.0.3) turbo: - specifier: ^1.9.3 - version: 1.9.3 + specifier: ^1.9.8 + version: 1.9.8 typescript: specifier: 5.0.3 version: 5.0.3(patch_hash=3z36spodsd2tfiihsln6xdxzra) @@ -6623,65 +6623,65 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64@1.9.3: - resolution: {integrity: sha512-0dFc2cWXl82kRE4Z+QqPHhbEFEpUZho1msHXHWbz5+PqLxn8FY0lEVOHkq5tgKNNEd5KnGyj33gC/bHhpZOk5g==} + /turbo-darwin-64@1.9.8: + resolution: {integrity: sha512-PkTdBjPfgpj/Dob/6SjkzP0BBP80/KmFjLEocXVEECCLJE6tHKbWLRdvc79B0N6SufdYdZ1uvvoU3KPtBokSPw==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.9.3: - resolution: {integrity: sha512-1cYbjqLBA2zYE1nbf/qVnEkrHa4PkJJbLo7hnuMuGM0bPzh4+AnTNe98gELhqI1mkTWBu/XAEeF5u6dgz0jLNA==} + /turbo-darwin-arm64@1.9.8: + resolution: {integrity: sha512-sLwqOx3XV57QCEoJM9GnDDnnqidG8wf29ytxssBaWHBdeJTjupyrmzTUrX+tyKo3Q+CjWvbPLyqVqxT4g5NuXQ==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.9.3: - resolution: {integrity: sha512-UuBPFefawEwpuxh5pM9Jqq3q4C8M0vYxVYlB3qea/nHQ80pxYq7ZcaLGEpb10SGnr3oMUUs1zZvkXWDNKCJb8Q==} + /turbo-linux-64@1.9.8: + resolution: {integrity: sha512-AMg6VT6sW7aOD1uOs5suxglXfTYz9T0uVyKGKokDweGOYTWmuTMGU5afUT1tYRUwQ+kVPJI+83Atl5Ob0oBsgw==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.9.3: - resolution: {integrity: sha512-vUrNGa3hyDtRh9W0MkO+l1dzP8Co2gKnOVmlJQW0hdpOlWlIh22nHNGGlICg+xFa2f9j4PbQlWTsc22c019s8Q==} + /turbo-linux-arm64@1.9.8: + resolution: {integrity: sha512-tLnxFv+OIklwTjiOZ8XMeEeRDAf150Ry4BCivNwgTVFAqQGEqkFP6KGBy56hb5RRF1frPQpoPGipJNVm7c8m1w==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.9.3: - resolution: {integrity: sha512-0BZ7YaHs6r+K4ksqWus1GKK3W45DuDqlmfjm/yuUbTEVc8szmMCs12vugU2Zi5GdrdJSYfoKfEJ/PeegSLIQGQ==} + /turbo-windows-64@1.9.8: + resolution: {integrity: sha512-r3pCjvXTMR7kq2E3iqwFlN1R7pFO/TOsuUjMhOSPP7HwuuUIinAckU4I9foM3q7ZCQd1XXScBUt3niDyHijAqQ==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.9.3: - resolution: {integrity: sha512-QJUYLSsxdXOsR1TquiOmLdAgtYcQ/RuSRpScGvnZb1hY0oLc7JWU0llkYB81wVtWs469y8H9O0cxbKwCZGR4RQ==} + /turbo-windows-arm64@1.9.8: + resolution: {integrity: sha512-CWzRbX2TM5IfHBC6uWM659qUOEDC4h0nn16ocG8yIq1IF3uZMzKRBHgGOT5m1BHom+R08V0NcjTmPRoqpiI0dg==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.9.3: - resolution: {integrity: sha512-ID7mxmaLUPKG/hVkp+h0VuucB1U99RPCJD9cEuSEOdIPoSIuomcIClEJtKamUsdPLhLCud+BvapBNnhgh58Nzw==} + /turbo@1.9.8: + resolution: {integrity: sha512-dTouGZBm4a2fE0OPafcTQERCp4i3ZOow0Pr0JlOyxKmzJy0JRwXypH013kbZoK6k1ET5tS/g9rwUXIM/AmWXXQ==} hasBin: true requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.9.3 - turbo-darwin-arm64: 1.9.3 - turbo-linux-64: 1.9.3 - turbo-linux-arm64: 1.9.3 - turbo-windows-64: 1.9.3 - turbo-windows-arm64: 1.9.3 + turbo-darwin-64: 1.9.8 + turbo-darwin-arm64: 1.9.8 + turbo-linux-64: 1.9.8 + turbo-linux-arm64: 1.9.8 + turbo-windows-64: 1.9.8 + turbo-windows-arm64: 1.9.8 dev: true /tweetnacl@0.14.5: From d6bd57e825737a29be1f37c56eb62ead76ecd27f Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Mon, 22 May 2023 15:09:09 +0300 Subject: [PATCH 04/10] Bump ORM version --- changelogs/drizzle-orm/0.26.1.md | 1 + drizzle-orm/package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelogs/drizzle-orm/0.26.1.md diff --git a/changelogs/drizzle-orm/0.26.1.md b/changelogs/drizzle-orm/0.26.1.md new file mode 100644 index 000000000..9877e829c --- /dev/null +++ b/changelogs/drizzle-orm/0.26.1.md @@ -0,0 +1 @@ +- 🐛 Fixed including multiple relations on the same level in RQB (#599) diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index aa5ec506d..5f4e42023 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.26.0", + "version": "0.26.1", "description": "Drizzle ORM package for SQL databases", "type": "module", "scripts": { From 831dbf359d8ebfb2e71aa6939d00f393b21b1e5c Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Tue, 23 May 2023 15:55:16 +0300 Subject: [PATCH 05/10] Add "[" value appeared case for deep with Add duplicate case test for pg --- .../mysql}/mysql.duplicates.test.ts | 0 .../mysql}/mysql.duplicates.ts | 0 .../duplicates/pg/pg.duplicates.test.ts | 210 ++++++++++++ .../duplicates/pg/pg.duplicates.ts | 86 +++++ .../issues-schemas/wrong-mapping/pg.schema.ts | 156 +++++++++ .../issues-schemas/wrong-mapping/pg.test.ts | 321 ++++++++++++++++++ 6 files changed, 773 insertions(+) rename integration-tests/tests/relational/issues-schemas/{ => duplicates/mysql}/mysql.duplicates.test.ts (100%) rename integration-tests/tests/relational/issues-schemas/{ => duplicates/mysql}/mysql.duplicates.ts (100%) create mode 100644 integration-tests/tests/relational/issues-schemas/duplicates/pg/pg.duplicates.test.ts create mode 100644 integration-tests/tests/relational/issues-schemas/duplicates/pg/pg.duplicates.ts create mode 100644 integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.schema.ts create mode 100644 integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts diff --git a/integration-tests/tests/relational/issues-schemas/mysql.duplicates.test.ts b/integration-tests/tests/relational/issues-schemas/duplicates/mysql/mysql.duplicates.test.ts similarity index 100% rename from integration-tests/tests/relational/issues-schemas/mysql.duplicates.test.ts rename to integration-tests/tests/relational/issues-schemas/duplicates/mysql/mysql.duplicates.test.ts diff --git a/integration-tests/tests/relational/issues-schemas/mysql.duplicates.ts b/integration-tests/tests/relational/issues-schemas/duplicates/mysql/mysql.duplicates.ts similarity index 100% rename from integration-tests/tests/relational/issues-schemas/mysql.duplicates.ts rename to integration-tests/tests/relational/issues-schemas/duplicates/mysql/mysql.duplicates.ts diff --git a/integration-tests/tests/relational/issues-schemas/duplicates/pg/pg.duplicates.test.ts b/integration-tests/tests/relational/issues-schemas/duplicates/pg/pg.duplicates.test.ts new file mode 100644 index 000000000..a45d60f4a --- /dev/null +++ b/integration-tests/tests/relational/issues-schemas/duplicates/pg/pg.duplicates.test.ts @@ -0,0 +1,210 @@ +import 'dotenv/config'; +import Docker from 'dockerode'; +import { sql } from 'drizzle-orm'; +import getPort from 'get-port'; +import { v4 as uuid } from 'uuid'; +import { afterAll, beforeAll, beforeEach, expect, expectTypeOf, test } from 'vitest'; +import * as schema from './pg.duplicates'; +import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Client } from 'pg'; + +const ENABLE_LOGGING = false; + +/* + Test cases: + - querying nested relation without PK with additional fields +*/ + +let pgContainer: Docker.Container; +let db: NodePgDatabase; +let client: Client; + +async function createDockerDB(): Promise { + const docker = new Docker(); + const port = await getPort({ port: 5432 }); + const image = 'postgres:14'; + + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => err ? reject(err) : resolve(err)) + ); + + pgContainer = await docker.createContainer({ + Image: image, + Env: [ + 'POSTGRES_PASSWORD=postgres', + 'POSTGRES_USER=postgres', + 'POSTGRES_DB=postgres', + ], + name: `drizzle-integration-tests-${uuid()}`, + HostConfig: { + AutoRemove: true, + PortBindings: { + '5432/tcp': [{ HostPort: `${port}` }], + }, + }, + }); + + await pgContainer.start(); + + return `postgres://postgres:postgres@localhost:${port}/postgres`; +} + +beforeAll(async () => { + const connectionString = process.env['PG_CONNECTION_STRING'] ?? (await createDockerDB()); + + const sleep = 250; + let timeLeft = 5000; + let connected = false; + let lastError: unknown | undefined; + do { + try { + client = new Client(connectionString); + await client.connect(); + connected = true; + break; + } catch (e) { + lastError = e; + await new Promise((resolve) => setTimeout(resolve, sleep)); + timeLeft -= sleep; + } + } while (timeLeft > 0); + if (!connected) { + console.error('Cannot connect to Postgres'); + await client?.end().catch(console.error); + await pgContainer?.stop().catch(console.error); + throw lastError; + } + db = drizzle(client, { schema, logger: ENABLE_LOGGING }); +}); + +afterAll(async () => { + await client?.end().catch(console.error); + await pgContainer?.stop().catch(console.error); +}); + +beforeEach(async () => { + await db.execute(sql`drop table if exists "members"`); + await db.execute(sql`drop table if exists "artist_to_member"`); + await db.execute(sql`drop table if exists "artists"`); + await db.execute(sql`drop table if exists "albums"`); + + await db.execute( + sql` + CREATE TABLE "members" ( + "id" serial PRIMARY KEY NOT NULL, + "created_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `, + ); + await db.execute( + sql` + CREATE TABLE "artist_to_member" ( + "id" serial PRIMARY KEY NOT NULL, + "member_id" int NOT NULL, + "artist_id" int NOT NULL); + `, + ); + await db.execute( + sql` + CREATE TABLE "artists" ( + "id" serial PRIMARY KEY NOT NULL, + "created_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "company_id" int NOT NULL); + `, + ); + await db.execute( + sql` + CREATE TABLE "albums" ( + "id" serial PRIMARY KEY NOT NULL, + "created_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "artist_id" int NOT NULL); + `, + ); +}); + +test('Simple case from GH', async () => { + await db.insert(schema.artists).values([ + { + id: 1, + companyId: 1, + }, + { + id: 2, + companyId: 1, + }, + { + id: 3, + companyId: 1, + }, + ]); + + await db.insert(schema.albums).values([ + { id: 1, artistId: 1 }, + { id: 2, artistId: 2 }, + { id: 3, artistId: 3 }, + ]); + + await db.insert(schema.members).values([ + { id: 1 }, + { id: 2 }, + { id: 3 }, + ]); + + await db.insert(schema.artistsToMembers).values([ + { memberId: 1, artistId: 1 }, + { memberId: 2, artistId: 1 }, + { memberId: 2, artistId: 2 }, + { memberId: 3, artistId: 3 }, + ]); + + const response = await db.query.artists.findFirst({ + where: (artists, { eq }) => eq(artists.id, 1), + with: { + albums: true, + members: { + columns: {}, + with: { + member: true, + }, + }, + }, + }); + + console.log(JSON.stringify(response, null, 2)); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + createdAt: Date; + updatedAt: Date; + companyId: number; + albums: { + id: number; + createdAt: Date; + updatedAt: Date; + artistId: number; + }[]; + members: { + member: { + id: number; + createdAt: Date; + updatedAt: Date; + }; + }[]; + } | undefined + >(); + + expect(response?.members.length).eq(2); + expect(response?.albums.length).eq(1); + + expect(response?.albums[0]).toEqual({ + id: 1, + createdAt: response?.albums[0]?.createdAt, + updatedAt: response?.albums[0]?.updatedAt, + artistId: 1, + }); +}); diff --git a/integration-tests/tests/relational/issues-schemas/duplicates/pg/pg.duplicates.ts b/integration-tests/tests/relational/issues-schemas/duplicates/pg/pg.duplicates.ts new file mode 100644 index 000000000..c8501a1f7 --- /dev/null +++ b/integration-tests/tests/relational/issues-schemas/duplicates/pg/pg.duplicates.ts @@ -0,0 +1,86 @@ +import { relations, sql } from "drizzle-orm"; +import { pgTable, index, integer, serial, timestamp } from "drizzle-orm/pg-core"; + +export const artists = pgTable( + 'artists', + { + id: serial('id').primaryKey(), + createdAt: timestamp('created_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: timestamp('updated_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + companyId: integer('company_id').notNull(), + } +); + +export const members = pgTable('members', { + id: serial('id').primaryKey(), + createdAt: timestamp('created_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: timestamp('updated_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const artistsToMembers = pgTable( + 'artist_to_member', + { + id: serial('id').primaryKey(), + memberId: integer('member_id').notNull(), + artistId: integer('artist_id').notNull(), + }, + (table) => ({ + memberArtistIndex: index('artist_to_member__artist_id__member_id__idx').on( + table.memberId, + table.artistId, + ), + }), +); + +export const albums = pgTable( + 'albums', + { + id: serial('id').primaryKey(), + createdAt: timestamp('created_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: timestamp('updated_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + artistId: integer('artist_id').notNull(), + }, + (table) => ({ + artistIndex: index('albums__artist_id__idx').on(table.artistId), + }), +); + +// relations +export const artistRelations = relations(artists, ({ many }) => ({ + albums: many(albums), + members: many(artistsToMembers), +})); + +export const albumRelations = relations(albums, ({ one }) => ({ + artist: one(artists, { + fields: [albums.artistId], + references: [artists.id], + }), +})); + +export const memberRelations = relations(members, ({ many }) => ({ + artists: many(artistsToMembers), +})); + +export const artistsToMembersRelations = relations(artistsToMembers, ({ one }) => ({ + artist: one(artists, { + fields: [artistsToMembers.artistId], + references: [artists.id], + }), + member: one(members, { + fields: [artistsToMembers.memberId], + references: [members.id], + }), +})); diff --git a/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.schema.ts b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.schema.ts new file mode 100644 index 000000000..44b2fe964 --- /dev/null +++ b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.schema.ts @@ -0,0 +1,156 @@ +import { relations } from "drizzle-orm"; +import { boolean, integer, pgTable, primaryKey, text, uuid } from "drizzle-orm/pg-core"; + +export const menuItems = pgTable('menu_items', { + id: uuid('id').defaultRandom().primaryKey(), +}); + +export const modifierGroups = pgTable('modifier_groups', { + id: uuid('id').defaultRandom().primaryKey(), +}); + +export const menuItemModifierGroups = pgTable( + 'menu_item_modifier_groups', + { + menuItemId: uuid('menu_item_id') + .notNull() + .references(() => menuItems.id), + modifierGroupId: uuid('modifier_group_id') + .notNull() + .references(() => modifierGroups.id), + order: integer('order').default(0), + }, + (table) => ({ + menuItemIdModifierGroupIdOrderPk: primaryKey( + table.menuItemId, + table.modifierGroupId, + table.order, + ), + }), +); + +export const ingredients = pgTable('ingredients', { + id: uuid('id').defaultRandom().primaryKey(), + name: text('name').notNull(), + description: text('description'), + imageUrl: text('image_url'), + inStock: boolean('in_stock').default(true), +}); + +export const modifiers = pgTable('modifiers', { + id: uuid('id').defaultRandom().primaryKey(), + ingredientId: uuid('ingredient_id').references(() => ingredients.id), + itemId: uuid('item_id').references(() => menuItems.id), +}); + +export const menuItemIngredients = pgTable( + 'menu_item_ingredients', + { + menuItemId: uuid('menu_item_id') + .notNull() + .references(() => menuItems.id), + ingredientId: uuid('ingredient_id') + .notNull() + .references(() => ingredients.id), + order: integer('order').default(0), + }, + (table) => ({ + menuItemIdIngredientIdOrderPk: primaryKey( + table.menuItemId, + table.ingredientId, + table.order, + ), + }), +); + +export const modifierGroupModifiers = pgTable( + 'modifier_group_modifiers', + { + modifierGroupId: uuid('modifier_group_id') + .notNull() + .references(() => modifierGroups.id), + modifierId: uuid('modifier_id') + .notNull() + .references(() => modifiers.id), + order: integer('order').default(0), + }, + (table) => ({ + modifierGroupIdModifierIdOrderPk: primaryKey( + table.modifierGroupId, + table.modifierId, + table.order, + ), + }), +); + +export const menuItemRelations = relations(menuItems, ({ many }) => ({ + ingredients: many(menuItemIngredients), + modifierGroups: many(menuItemModifierGroups), + // category: one(menuCategories, { + // fields: [menuItems.categoryId], + // references: [menuCategories.id], + // }), +})); + +export const menuItemIngredientRelations = relations( + menuItemIngredients, + ({ one }) => ({ + menuItem: one(menuItems, { + fields: [menuItemIngredients.menuItemId], + references: [menuItems.id], + }), + ingredient: one(ingredients, { + fields: [menuItemIngredients.ingredientId], + references: [ingredients.id], + }), + }), +); + +export const ingredientRelations = relations(ingredients, ({ many }) => ({ + menuItems: many(menuItemIngredients), +})); + +export const modifierGroupRelations = relations(modifierGroups, ({ many }) => ({ + menuItems: many(menuItemModifierGroups), + modifiers: many(modifierGroupModifiers), +})); + +export const modifierRelations = relations(modifiers, ({ one, many }) => ({ + modifierGroups: many(modifierGroupModifiers), + ingredient: one(ingredients, { + fields: [modifiers.ingredientId], + references: [ingredients.id], + }), + item: one(menuItems, { + fields: [modifiers.itemId], + references: [menuItems.id], + }), +})); + +export const menuItemModifierGroupRelations = relations( + menuItemModifierGroups, + ({ one }) => ({ + menuItem: one(menuItems, { + fields: [menuItemModifierGroups.menuItemId], + references: [menuItems.id], + }), + modifierGroup: one(modifierGroups, { + fields: [menuItemModifierGroups.modifierGroupId], + references: [modifierGroups.id], + }), + }), +); + +export const modifierGroupModifierRelations = relations( + modifierGroupModifiers, + ({ one }) => ({ + modifierGroup: one(modifierGroups, { + fields: [modifierGroupModifiers.modifierGroupId], + references: [modifierGroups.id], + }), + modifier: one(modifiers, { + fields: [modifierGroupModifiers.modifierId], + references: [modifiers.id], + }), + }), +); diff --git a/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts new file mode 100644 index 000000000..36de0188a --- /dev/null +++ b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts @@ -0,0 +1,321 @@ +import 'dotenv/config'; +import Docker from 'dockerode'; +import { desc, sql } from 'drizzle-orm'; +import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; +import getPort from 'get-port'; +import { Client } from 'pg'; +import { v4 as uuid } from 'uuid'; +import { afterAll, beforeAll, beforeEach, expect, expectTypeOf, test } from 'vitest'; +import * as schema from './pg.schema'; + +const ENABLE_LOGGING = false; + +/* + Test cases: + - querying nested relation without PK with additional fields +*/ + +let pgContainer: Docker.Container; +let db: NodePgDatabase; +let client: Client; + +async function createDockerDB(): Promise { + const docker = new Docker(); + const port = await getPort({ port: 5432 }); + const image = 'postgres:14'; + + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => err ? reject(err) : resolve(err)) + ); + + pgContainer = await docker.createContainer({ + Image: image, + Env: [ + 'POSTGRES_PASSWORD=postgres', + 'POSTGRES_USER=postgres', + 'POSTGRES_DB=postgres', + ], + name: `drizzle-integration-tests-${uuid()}`, + HostConfig: { + AutoRemove: true, + PortBindings: { + '5432/tcp': [{ HostPort: `${port}` }], + }, + }, + }); + + await pgContainer.start(); + + return `postgres://postgres:postgres@localhost:${port}/postgres`; +} + +beforeAll(async () => { + const connectionString = process.env['PG_CONNECTION_STRING'] ?? (await createDockerDB()); + + const sleep = 250; + let timeLeft = 5000; + let connected = false; + let lastError: unknown | undefined; + do { + try { + client = new Client(connectionString); + await client.connect(); + connected = true; + break; + } catch (e) { + lastError = e; + await new Promise((resolve) => setTimeout(resolve, sleep)); + timeLeft -= sleep; + } + } while (timeLeft > 0); + if (!connected) { + console.error('Cannot connect to Postgres'); + await client?.end().catch(console.error); + await pgContainer?.stop().catch(console.error); + throw lastError; + } + db = drizzle(client, { schema, logger: ENABLE_LOGGING }); +}); + +afterAll(async () => { + await client?.end().catch(console.error); + await pgContainer?.stop().catch(console.error); +}); + +beforeEach(async () => { + await db.execute(sql`drop schema public cascade`); + await db.execute(sql`create schema public`); + + await db.execute( + sql` + CREATE TABLE IF NOT EXISTS "ingredients" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "description" text, + "image_url" text, + "in_stock" boolean DEFAULT true + ); + + CREATE TABLE IF NOT EXISTS "menu_item_ingredients" ( + "menu_item_id" uuid NOT NULL, + "ingredient_id" uuid NOT NULL, + "order" integer DEFAULT 0 + ); + + ALTER TABLE "menu_item_ingredients" ADD CONSTRAINT "menu_item_ingredients_menu_item_id_ingredient_id_order" PRIMARY KEY("menu_item_id","ingredient_id","order"); + + CREATE TABLE IF NOT EXISTS "menu_item_modifier_groups" ( + "menu_item_id" uuid NOT NULL, + "modifier_group_id" uuid NOT NULL, + "order" integer DEFAULT 0 + ); + + ALTER TABLE "menu_item_modifier_groups" ADD CONSTRAINT "menu_item_modifier_groups_menu_item_id_modifier_group_id_order" PRIMARY KEY("menu_item_id","modifier_group_id","order"); + + CREATE TABLE IF NOT EXISTS "menu_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "modifier_group_modifiers" ( + "modifier_group_id" uuid NOT NULL, + "modifier_id" uuid NOT NULL, + "order" integer DEFAULT 0 + ); + + ALTER TABLE "modifier_group_modifiers" ADD CONSTRAINT "modifier_group_modifiers_modifier_group_id_modifier_id_order" PRIMARY KEY("modifier_group_id","modifier_id","order"); + + CREATE TABLE IF NOT EXISTS "modifier_groups" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "modifiers" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "ingredient_id" uuid, + "item_id" uuid + ); + + DO $$ BEGIN + ALTER TABLE "menu_item_ingredients" ADD CONSTRAINT "menu_item_ingredients_menu_item_id_menu_items_id_fk" FOREIGN KEY ("menu_item_id") REFERENCES "menu_items"("id") ON DELETE no action ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "menu_item_ingredients" ADD CONSTRAINT "menu_item_ingredients_ingredient_id_ingredients_id_fk" FOREIGN KEY ("ingredient_id") REFERENCES "ingredients"("id") ON DELETE no action ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "menu_item_modifier_groups" ADD CONSTRAINT "menu_item_modifier_groups_menu_item_id_menu_items_id_fk" FOREIGN KEY ("menu_item_id") REFERENCES "menu_items"("id") ON DELETE no action ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "menu_item_modifier_groups" ADD CONSTRAINT "menu_item_modifier_groups_modifier_group_id_modifier_groups_id_fk" FOREIGN KEY ("modifier_group_id") REFERENCES "modifier_groups"("id") ON DELETE no action ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "modifier_group_modifiers" ADD CONSTRAINT "modifier_group_modifiers_modifier_group_id_modifier_groups_id_fk" FOREIGN KEY ("modifier_group_id") REFERENCES "modifier_groups"("id") ON DELETE no action ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "modifier_group_modifiers" ADD CONSTRAINT "modifier_group_modifiers_modifier_id_modifiers_id_fk" FOREIGN KEY ("modifier_id") REFERENCES "modifiers"("id") ON DELETE no action ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "modifiers" ADD CONSTRAINT "modifiers_ingredient_id_ingredients_id_fk" FOREIGN KEY ("ingredient_id") REFERENCES "ingredients"("id") ON DELETE no action ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "modifiers" ADD CONSTRAINT "modifiers_item_id_menu_items_id_fk" FOREIGN KEY ("item_id") REFERENCES "menu_items"("id") ON DELETE no action ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + `, + ); +}); + +test('Simple case from GH', async () => { + const firstMenuItemId = uuid(); + const secondMenuItemId = uuid(); + + const firstModGroupsId = uuid(); + const secondModGroupsId = uuid(); + + await db.insert(schema.menuItems).values([{ id: firstMenuItemId }, { id: secondMenuItemId }]); + await db.insert(schema.modifierGroups).values([{ id: firstModGroupsId }, { id: secondModGroupsId }]); + await db.insert(schema.menuItemModifierGroups).values([{ + modifierGroupId: firstModGroupsId, + menuItemId: firstMenuItemId, + }, { + modifierGroupId: firstModGroupsId, + menuItemId: secondMenuItemId, + }, { + modifierGroupId: secondModGroupsId, + menuItemId: firstMenuItemId, + }]); + + const firstIngredientId = uuid(); + const secondIngredientId = uuid(); + + await db.insert(schema.ingredients).values([{ + id: firstIngredientId, + name: 'first', + }, { + id: secondIngredientId, + name: 'second', + }]); + + const firstModifierId = uuid(); + const secondModifierId = uuid(); + + console.log('firstModifierId: ', firstModifierId) + console.log('secondModifierId: ', secondModifierId) + + console.log('ing1: ', firstIngredientId) + console.log('ing2: ', secondIngredientId) + + console.log('f1: ', firstMenuItemId) + console.log('f2: ', secondMenuItemId) + + await db.insert(schema.modifiers).values([{ + id: firstModifierId, + ingredientId: firstIngredientId, + itemId: firstMenuItemId, + }, { + id: secondModifierId, + ingredientId: secondIngredientId, + itemId: secondMenuItemId, + }]); + + await db.insert(schema.modifierGroupModifiers).values([ + { + modifierGroupId: firstModGroupsId, + modifierId: firstModifierId, + }, + { + modifierGroupId: secondModGroupsId, + modifierId: secondModifierId, + }, + ]); + + const response = await db.query.menuItems + .findMany({ + with: { + modifierGroups: { + with: { + modifierGroup: { + with: { + modifiers: { + with: { + modifier: { + with: { + ingredient: true, + item: true, + }, + }, + }, + orderBy: desc(schema.modifierGroupModifiers.order), + }, + }, + }, + }, + orderBy: schema.menuItemModifierGroups.order, + }, + }, + }); + + console.log(JSON.stringify(response, null, 2)); + + expectTypeOf(response).toEqualTypeOf< + { + id: string; + modifierGroups: { + menuItemId: string; + modifierGroupId: string; + order: number | null; + modifierGroup: { + id: string; + modifiers: { + modifierGroupId: string; + order: number | null; + modifierId: string; + modifier: { + id: string; + ingredientId: string | null; + itemId: string | null; + ingredient: { + id: string; + name: string; + description: string | null; + imageUrl: string | null; + inStock: boolean | null; + } | null; + item: { + id: string; + } | null; + }; + }[]; + }; + }[]; + }[] + >(); + + expect(response.length).eq(2) + expect(response[0]?.modifierGroups.length).eq(2) + expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers.length).eq(1) + + expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers[0]?.modifier.ingredient?.id).eq('0b2b9abc-5975-4a1d-ba3d-6fc3b3149902') + expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers[0]?.modifier.item?.id).eq('a867133e-60b7-4003-aaa0-deeefad7e518') +}); From b6431a4b863eb86c7ad9687633e30231c9fdeec2 Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Tue, 23 May 2023 15:57:01 +0300 Subject: [PATCH 06/10] Fix expect size for wrong mapping test --- .../tests/relational/issues-schemas/wrong-mapping/pg.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts index 36de0188a..5c9f0ea15 100644 --- a/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts +++ b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts @@ -313,7 +313,7 @@ test('Simple case from GH', async () => { >(); expect(response.length).eq(2) - expect(response[0]?.modifierGroups.length).eq(2) + expect(response[0]?.modifierGroups.length).eq(1) expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers.length).eq(1) expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers[0]?.modifier.ingredient?.id).eq('0b2b9abc-5975-4a1d-ba3d-6fc3b3149902') From c9b44b199509366865fcaeee5072e58fd087a62d Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Tue, 23 May 2023 23:30:56 +0300 Subject: [PATCH 07/10] Fix RQB query generation for Postgres --- drizzle-orm/src/pg-core/dialect.ts | 7 +++-- .../issues-schemas/wrong-mapping/pg.test.ts | 26 +++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index d4b07fd90..2214d4f48 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -617,8 +617,11 @@ export class PgDialect { const elseField = sql`json_agg(json_build_array(${ sql.join( - builtRelation.selection.map(({ dbKey: key }) => { - const field = sql`${sql.identifier(relationAlias)}.${sql.identifier(key)}`; + builtRelation.selection.map(({ dbKey: key, isJson }) => { + let field = sql`${sql.identifier(relationAlias)}.${sql.identifier(key)}`; + if (isJson) { + field = sql`${field}::json`; + } return field; }), sql`, `, diff --git a/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts index 5c9f0ea15..a5982bb2f 100644 --- a/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts +++ b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts @@ -220,14 +220,14 @@ test('Simple case from GH', async () => { const firstModifierId = uuid(); const secondModifierId = uuid(); - console.log('firstModifierId: ', firstModifierId) - console.log('secondModifierId: ', secondModifierId) + console.log('firstModifierId:', firstModifierId); + console.log('secondModifierId:', secondModifierId); - console.log('ing1: ', firstIngredientId) - console.log('ing2: ', secondIngredientId) + console.log('ing1:', firstIngredientId); + console.log('ing2:', secondIngredientId); - console.log('f1: ', firstMenuItemId) - console.log('f2: ', secondMenuItemId) + console.log('f1:', firstMenuItemId); + console.log('f2:', secondMenuItemId); await db.insert(schema.modifiers).values([{ id: firstModifierId, @@ -312,10 +312,14 @@ test('Simple case from GH', async () => { }[] >(); - expect(response.length).eq(2) - expect(response[0]?.modifierGroups.length).eq(1) - expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers.length).eq(1) + expect(response.length).eq(2); + expect(response[0]?.modifierGroups.length).eq(1); + expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers.length).eq(1); - expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers[0]?.modifier.ingredient?.id).eq('0b2b9abc-5975-4a1d-ba3d-6fc3b3149902') - expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers[0]?.modifier.item?.id).eq('a867133e-60b7-4003-aaa0-deeefad7e518') + expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers[0]?.modifier.ingredient?.id).eq( + '0b2b9abc-5975-4a1d-ba3d-6fc3b3149902', + ); + expect(response[0]?.modifierGroups[0]?.modifierGroup.modifiers[0]?.modifier.item?.id).eq( + 'a867133e-60b7-4003-aaa0-deeefad7e518', + ); }); From 9ff3829fc1fc3d8af97780cce9f36ad0cde48aea Mon Sep 17 00:00:00 2001 From: Maston <22679886+mastondzn@users.noreply.github.com> Date: Wed, 24 May 2023 12:22:16 +0300 Subject: [PATCH 08/10] Prefer `TSchema` approach --- drizzle-orm/src/aws-data-api/pg/migrator.ts | 7 ++++--- drizzle-orm/src/better-sqlite3/migrator.ts | 7 ++++--- drizzle-orm/src/bun-sqlite/migrator.ts | 4 ++-- drizzle-orm/src/d1/migrator.ts | 7 ++++--- drizzle-orm/src/libsql/migrator.ts | 4 ++-- drizzle-orm/src/mysql2/migrator.ts | 7 ++++--- drizzle-orm/src/neon-serverless/migrator.ts | 4 ++-- drizzle-orm/src/node-postgres/migrator.ts | 7 ++++--- drizzle-orm/src/planetscale-serverless/migrator.ts | 7 ++++--- drizzle-orm/src/postgres-js/migrator.ts | 7 ++++--- drizzle-orm/src/sql-js/migrator.ts | 4 ++-- drizzle-orm/src/sqlite-proxy/migrator.ts | 8 +++++--- drizzle-orm/src/vercel-postgres/migrator.ts | 7 ++++--- 13 files changed, 45 insertions(+), 35 deletions(-) diff --git a/drizzle-orm/src/aws-data-api/pg/migrator.ts b/drizzle-orm/src/aws-data-api/pg/migrator.ts index e2c66bb42..d4aef70f1 100644 --- a/drizzle-orm/src/aws-data-api/pg/migrator.ts +++ b/drizzle-orm/src/aws-data-api/pg/migrator.ts @@ -2,9 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { AwsDataApiPgDatabase } from './driver'; -export async function migrate< - T extends AwsDataApiPgDatabase>, ->(db: T, config: string | MigrationConfig) { +export async function migrate>( + db: AwsDataApiPgDatabase, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/better-sqlite3/migrator.ts b/drizzle-orm/src/better-sqlite3/migrator.ts index c5d85659a..14802b62c 100644 --- a/drizzle-orm/src/better-sqlite3/migrator.ts +++ b/drizzle-orm/src/better-sqlite3/migrator.ts @@ -2,9 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { BetterSQLite3Database } from './driver'; -export function migrate< - T extends BetterSQLite3Database>, ->(db: T, config: string | MigrationConfig) { +export function migrate>( + db: BetterSQLite3Database, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/bun-sqlite/migrator.ts b/drizzle-orm/src/bun-sqlite/migrator.ts index ac4e8856b..4e9c17506 100644 --- a/drizzle-orm/src/bun-sqlite/migrator.ts +++ b/drizzle-orm/src/bun-sqlite/migrator.ts @@ -2,8 +2,8 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { BunSQLiteDatabase } from './driver'; -export function migrate>>( - db: T, +export function migrate>( + db: BunSQLiteDatabase, config: string | MigrationConfig, ) { const migrations = readMigrationFiles(config); diff --git a/drizzle-orm/src/d1/migrator.ts b/drizzle-orm/src/d1/migrator.ts index 675e212e3..c6465d88e 100644 --- a/drizzle-orm/src/d1/migrator.ts +++ b/drizzle-orm/src/d1/migrator.ts @@ -2,9 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { DrizzleD1Database } from './driver'; -export async function migrate< - T extends DrizzleD1Database>, ->(db: T, config: string | MigrationConfig) { +export async function migrate>( + db: DrizzleD1Database, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/libsql/migrator.ts b/drizzle-orm/src/libsql/migrator.ts index 41d681f3d..45631413a 100644 --- a/drizzle-orm/src/libsql/migrator.ts +++ b/drizzle-orm/src/libsql/migrator.ts @@ -2,8 +2,8 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { LibSQLDatabase } from './driver'; -export function migrate>>( - db: T, +export function migrate>( + db: LibSQLDatabase, config: MigrationConfig, ) { const migrations = readMigrationFiles(config); diff --git a/drizzle-orm/src/mysql2/migrator.ts b/drizzle-orm/src/mysql2/migrator.ts index e4fa7ff69..1b76545e5 100644 --- a/drizzle-orm/src/mysql2/migrator.ts +++ b/drizzle-orm/src/mysql2/migrator.ts @@ -2,9 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { MySql2Database } from './driver'; -export async function migrate< - T extends MySql2Database>, ->(db: T, config: MigrationConfig) { +export async function migrate>( + db: MySql2Database, + config: MigrationConfig, +) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session, config); } diff --git a/drizzle-orm/src/neon-serverless/migrator.ts b/drizzle-orm/src/neon-serverless/migrator.ts index 30dbfba3f..422702f26 100644 --- a/drizzle-orm/src/neon-serverless/migrator.ts +++ b/drizzle-orm/src/neon-serverless/migrator.ts @@ -2,8 +2,8 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { NeonDatabase } from './driver'; -export async function migrate>>( - db: T, +export async function migrate>( + db: NeonDatabase, config: string | MigrationConfig, ) { const migrations = readMigrationFiles(config); diff --git a/drizzle-orm/src/node-postgres/migrator.ts b/drizzle-orm/src/node-postgres/migrator.ts index cfbefb47e..da3fc7d10 100644 --- a/drizzle-orm/src/node-postgres/migrator.ts +++ b/drizzle-orm/src/node-postgres/migrator.ts @@ -2,9 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { NodePgDatabase } from './driver'; -export async function migrate< - T extends NodePgDatabase>, ->(db: T, config: string | MigrationConfig) { +export async function migrate>( + db: NodePgDatabase, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/planetscale-serverless/migrator.ts b/drizzle-orm/src/planetscale-serverless/migrator.ts index 417a5a1fc..52ba77992 100644 --- a/drizzle-orm/src/planetscale-serverless/migrator.ts +++ b/drizzle-orm/src/planetscale-serverless/migrator.ts @@ -2,9 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { PlanetScaleDatabase } from './driver'; -export async function migrate< - T extends PlanetScaleDatabase>, ->(db: T, config: MigrationConfig) { +export async function migrate>( + db: PlanetScaleDatabase, + config: MigrationConfig, +) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session, config); } diff --git a/drizzle-orm/src/postgres-js/migrator.ts b/drizzle-orm/src/postgres-js/migrator.ts index 09b528444..9fb6b1d73 100644 --- a/drizzle-orm/src/postgres-js/migrator.ts +++ b/drizzle-orm/src/postgres-js/migrator.ts @@ -2,9 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { PostgresJsDatabase } from './driver'; -export async function migrate< - T extends PostgresJsDatabase>, ->(db: T, config: string | MigrationConfig) { +export async function migrate>( + db: PostgresJsDatabase, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } diff --git a/drizzle-orm/src/sql-js/migrator.ts b/drizzle-orm/src/sql-js/migrator.ts index f858e9d0f..838b0b043 100644 --- a/drizzle-orm/src/sql-js/migrator.ts +++ b/drizzle-orm/src/sql-js/migrator.ts @@ -2,8 +2,8 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { SQLJsDatabase } from './driver'; -export function migrate>>( - db: T, +export function migrate>( + db: SQLJsDatabase, config: string | MigrationConfig, ) { const migrations = readMigrationFiles(config); diff --git a/drizzle-orm/src/sqlite-proxy/migrator.ts b/drizzle-orm/src/sqlite-proxy/migrator.ts index 7620d46f5..df2981a45 100644 --- a/drizzle-orm/src/sqlite-proxy/migrator.ts +++ b/drizzle-orm/src/sqlite-proxy/migrator.ts @@ -5,9 +5,11 @@ import type { SqliteRemoteDatabase } from './driver'; export type ProxyMigrator = (migrationQueries: string[]) => Promise; -export async function migrate< - T extends SqliteRemoteDatabase>, ->(db: T, callback: ProxyMigrator, config: string | MigrationConfig) { +export async function migrate>( + db: SqliteRemoteDatabase, + callback: ProxyMigrator, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); const migrationTableCreate = sql` diff --git a/drizzle-orm/src/vercel-postgres/migrator.ts b/drizzle-orm/src/vercel-postgres/migrator.ts index 7d86e82ff..80f6724e9 100644 --- a/drizzle-orm/src/vercel-postgres/migrator.ts +++ b/drizzle-orm/src/vercel-postgres/migrator.ts @@ -2,9 +2,10 @@ import type { MigrationConfig } from '~/migrator'; import { readMigrationFiles } from '~/migrator'; import type { VercelPgDatabase } from './driver'; -export async function migrate< - T extends VercelPgDatabase>, ->(db: T, config: string | MigrationConfig) { +export async function migrate>( + db: VercelPgDatabase, + config: string | MigrationConfig, +) { const migrations = readMigrationFiles(config); await db.dialect.migrate(migrations, db.session); } From f0e7daa33ddca112ce8e0901d8a47b5a4825c76d Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Wed, 24 May 2023 20:36:27 +0300 Subject: [PATCH 09/10] Fix .findMany() without arguments --- drizzle-orm/src/mysql-core/query-builders/query.ts | 2 +- drizzle-orm/src/pg-core/query-builders/query.ts | 2 +- drizzle-orm/src/sqlite-core/query-builders/query.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/query.ts b/drizzle-orm/src/mysql-core/query-builders/query.ts index 8ea771259..4e9801def 100644 --- a/drizzle-orm/src/mysql-core/query-builders/query.ts +++ b/drizzle-orm/src/mysql-core/query-builders/query.ts @@ -43,7 +43,7 @@ export class RelationalQueryBuilder< this.tableConfig, this.dialect, this.session, - config ? (config as DBQueryConfig<'many', true>) : true, + config ? (config as DBQueryConfig<'many', true>) : {}, 'many', ); } diff --git a/drizzle-orm/src/pg-core/query-builders/query.ts b/drizzle-orm/src/pg-core/query-builders/query.ts index 7e83a7688..7d018b500 100644 --- a/drizzle-orm/src/pg-core/query-builders/query.ts +++ b/drizzle-orm/src/pg-core/query-builders/query.ts @@ -34,7 +34,7 @@ export class RelationalQueryBuilder) : true, + config ? (config as DBQueryConfig<'many', true>) : {}, 'many', ); } diff --git a/drizzle-orm/src/sqlite-core/query-builders/query.ts b/drizzle-orm/src/sqlite-core/query-builders/query.ts index d49216421..72787c5c1 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/query.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/query.ts @@ -38,7 +38,7 @@ export class AsyncRelationalQueryBuilder< this.tableConfig, this.dialect, this.session, - config ? (config as DBQueryConfig<'many', true>) : true, + config ? (config as DBQueryConfig<'many', true>) : {}, 'many', ) as SQLiteAsyncRelationalQuery[]>; } @@ -93,7 +93,7 @@ export class SyncRelationalQueryBuilder< this.tableConfig, this.dialect, this.session, - config ? (config as DBQueryConfig<'many', true>) : true, + config ? (config as DBQueryConfig<'many', true>) : {}, 'many', ).prepare(); @@ -126,7 +126,7 @@ export class SyncRelationalQueryBuilder< this.tableConfig, this.dialect, this.session, - config ? (config as DBQueryConfig<'many', true>) : true, + config ? { ...(config as DBQueryConfig<'many', true> | undefined), limit: 1 } : { limit: 1 }, 'first', ).prepare(); From 1f1c16b95d2dcb8c029b200f72a5146a71f93cc0 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Wed, 24 May 2023 22:40:50 +0300 Subject: [PATCH 10/10] Update changelogs, fix .get() regression in SQLite --- changelogs/drizzle-orm/0.26.1.md | 2 ++ drizzle-orm/src/d1/session.ts | 2 +- drizzle-orm/src/libsql/session.ts | 2 +- integration-tests/package.json | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/changelogs/drizzle-orm/0.26.1.md b/changelogs/drizzle-orm/0.26.1.md index 9877e829c..838d91bda 100644 --- a/changelogs/drizzle-orm/0.26.1.md +++ b/changelogs/drizzle-orm/0.26.1.md @@ -1 +1,3 @@ - 🐛 Fixed including multiple relations on the same level in RQB (#599) +- 🐛 Updated migrators for relational queries support (#601) +- 🐛 Fixed invoking .findMany() without arguments diff --git a/drizzle-orm/src/d1/session.ts b/drizzle-orm/src/d1/session.ts index 1c6fc811c..66b9f903d 100644 --- a/drizzle-orm/src/d1/session.ts +++ b/drizzle-orm/src/d1/session.ts @@ -124,7 +124,7 @@ export class PreparedQuery if (!fields && !customResultMapper) { const params = fillPlaceholders(this.params, placeholderValues ?? {}); logger.logQuery(queryString, params); - return stmt.bind(...params).all().then(({ results }) => results!); + return stmt.bind(...params).all().then(({ results }) => results![0]); } const rows = await this.values(placeholderValues); diff --git a/drizzle-orm/src/libsql/session.ts b/drizzle-orm/src/libsql/session.ts index 5f028f591..9f707b3c1 100644 --- a/drizzle-orm/src/libsql/session.ts +++ b/drizzle-orm/src/libsql/session.ts @@ -142,7 +142,7 @@ export class PreparedQuery const params = fillPlaceholders(this.params, placeholderValues ?? {}); logger.logQuery(queryString, params); const stmt: InStatement = { sql: queryString, args: params as InArgs }; - return (tx ? tx.execute(stmt) : client.execute(stmt)).then(({ rows }) => rows.map((row) => normalizeRow(row))); + return (tx ? tx.execute(stmt) : client.execute(stmt)).then(({ rows }) => normalizeRow(rows[0])); } const rows = await this.values(placeholderValues); diff --git a/integration-tests/package.json b/integration-tests/package.json index 122212a9d..97b286537 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -70,4 +70,4 @@ "vitest": "^0.29.8", "zod": "^3.20.2" } -} \ No newline at end of file +}