diff --git a/README.md b/README.md
index 6aaa2d951..6cd78120f 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,18 @@
Drizzle ORM
-
+
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/changelogs/drizzle-orm/0.26.1.md b/changelogs/drizzle-orm/0.26.1.md
new file mode 100644
index 000000000..838d91bda
--- /dev/null
+++ b/changelogs/drizzle-orm/0.26.1.md
@@ -0,0 +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/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": {
diff --git a/drizzle-orm/src/aws-data-api/pg/migrator.ts b/drizzle-orm/src/aws-data-api/pg/migrator.ts
index 44bbf83fe..d4aef70f1 100644
--- a/drizzle-orm/src/aws-data-api/pg/migrator.ts
+++ b/drizzle-orm/src/aws-data-api/pg/migrator.ts
@@ -2,7 +2,10 @@ 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>(
+ 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 2b3574a6b..14802b62c 100644
--- a/drizzle-orm/src/better-sqlite3/migrator.ts
+++ b/drizzle-orm/src/better-sqlite3/migrator.ts
@@ -1,8 +1,11 @@
-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>(
+ db: BetterSQLite3Database,
+ config: string | MigrationConfig,
+) {
const migrations = readMigrationFiles(config);
db.dialect.migrate(migrations, db.session);
}
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/bun-sqlite/migrator.ts b/drizzle-orm/src/bun-sqlite/migrator.ts
index 22839fb03..4e9c17506 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: BunSQLiteDatabase,
+ 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..c6465d88e 100644
--- a/drizzle-orm/src/d1/migrator.ts
+++ b/drizzle-orm/src/d1/migrator.ts
@@ -1,8 +1,11 @@
-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>(
+ db: DrizzleD1Database,
+ config: string | MigrationConfig,
+) {
const migrations = readMigrationFiles(config);
await db.dialect.migrate(migrations, db.session);
}
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/migrator.ts b/drizzle-orm/src/libsql/migrator.ts
index a35c1ccbb..45631413a 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: LibSQLDatabase,
+ config: MigrationConfig,
+) {
const migrations = readMigrationFiles(config);
return db.dialect.migrate(migrations, db.session);
}
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/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..4e9801def 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 {
@@ -42,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',
);
}
@@ -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/mysql2/migrator.ts b/drizzle-orm/src/mysql2/migrator.ts
index 5f1dfb262..1b76545e5 100644
--- a/drizzle-orm/src/mysql2/migrator.ts
+++ b/drizzle-orm/src/mysql2/migrator.ts
@@ -2,7 +2,10 @@ 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>(
+ 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 0b273b71d..422702f26 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: NeonDatabase,
+ 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..da3fc7d10 100644
--- a/drizzle-orm/src/node-postgres/migrator.ts
+++ b/drizzle-orm/src/node-postgres/migrator.ts
@@ -1,8 +1,11 @@
-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>(
+ db: NodePgDatabase,
+ config: string | MigrationConfig,
+) {
const migrations = readMigrationFiles(config);
await db.dialect.migrate(migrations, db.session);
}
diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts
index f52570877..2214d4f48 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,26 +611,68 @@ 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(
- 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`, `,
)
- })${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 +682,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 +690,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 +725,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..7d018b500 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';
@@ -33,7 +34,7 @@ export class RelationalQueryBuilder) : true,
+ config ? (config as DBQueryConfig<'many', true>) : {},
'many',
);
}
@@ -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/planetscale-serverless/migrator.ts b/drizzle-orm/src/planetscale-serverless/migrator.ts
index d0a0a5168..52ba77992 100644
--- a/drizzle-orm/src/planetscale-serverless/migrator.ts
+++ b/drizzle-orm/src/planetscale-serverless/migrator.ts
@@ -2,7 +2,10 @@ 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>(
+ 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 7baea52f5..9fb6b1d73 100644
--- a/drizzle-orm/src/postgres-js/migrator.ts
+++ b/drizzle-orm/src/postgres-js/migrator.ts
@@ -1,8 +1,11 @@
-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>(
+ db: PostgresJsDatabase,
+ config: string | MigrationConfig,
+) {
const migrations = readMigrationFiles(config);
await db.dialect.migrate(migrations, db.session);
}
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/sql-js/migrator.ts b/drizzle-orm/src/sql-js/migrator.ts
index c192c16a6..838b0b043 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: SQLJsDatabase,
+ config: string | MigrationConfig,
+) {
const migrations = readMigrationFiles(config);
db.dialect.migrate(migrations, db.session);
}
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..72787c5c1 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';
@@ -37,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[]>;
}
@@ -92,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();
@@ -125,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();
@@ -169,7 +170,7 @@ export class SQLiteRelationalQuery Promise;
-export async function migrate(db: SqliteRemoteDatabase, callback: ProxyMigrator, config: string | MigrationConfig) {
+export async function migrate>(
+ db: SqliteRemoteDatabase,
+ callback: ProxyMigrator,
+ config: string | MigrationConfig,
+) {
const migrations = readMigrationFiles(config);
const migrationTableCreate = sql`
@@ -26,7 +30,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..80f6724e9 100644
--- a/drizzle-orm/src/vercel-postgres/migrator.ts
+++ b/drizzle-orm/src/vercel-postgres/migrator.ts
@@ -1,8 +1,11 @@
-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>(
+ db: VercelPgDatabase,
+ config: string | MigrationConfig,
+) {
const migrations = readMigrationFiles(config);
await db.dialect.migrate(migrations, db.session);
}
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
+}
diff --git a/integration-tests/tests/relational/bettersqlite.test.ts b/integration-tests/tests/relational/bettersqlite.test.ts
index 5ef97b0e7..825b7fd92 100644
--- a/integration-tests/tests/relational/bettersqlite.test.ts
+++ b/integration-tests/tests/relational/bettersqlite.test.ts
@@ -4055,7 +4055,7 @@ test('[Find Many] Get users with groups', async () => {
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/issues-schemas/duplicates/mysql/mysql.duplicates.test.ts b/integration-tests/tests/relational/issues-schemas/duplicates/mysql/mysql.duplicates.test.ts
new file mode 100644
index 000000000..f2d44bf33
--- /dev/null
+++ b/integration-tests/tests/relational/issues-schemas/duplicates/mysql/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/duplicates/mysql/mysql.duplicates.ts b/integration-tests/tests/relational/issues-schemas/duplicates/mysql/mysql.duplicates.ts
new file mode 100644
index 000000000..2f58f9ab3
--- /dev/null
+++ b/integration-tests/tests/relational/issues-schemas/duplicates/mysql/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/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..a5982bb2f
--- /dev/null
+++ b/integration-tests/tests/relational/issues-schemas/wrong-mapping/pg.test.ts
@@ -0,0 +1,325 @@
+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(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',
+ );
+});
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/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',
},
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: