drizzle-orm icon indicating copy to clipboard operation
drizzle-orm copied to clipboard

[BUG]: Nested object in select query

Open incredible007 opened this issue 1 year ago • 11 comments

What version of drizzle-orm are you using?

0.29.3

What version of drizzle-kit are you using?

0.20.14

Describe the Bug

My code is

    const feedbacks = await drizzleClient
        .select({
            ...getTableColumns(feedbackFirst),
            project: {
                ...getTableColumns(project),
            },
            userFrom: {
                ...getTableColumns(users),
                userAbout: {
                    ...getTableColumns(userAbout),
                    avatar: {
                        ...getTableColumns(avatars),
                        photo: {
                            ...getTableColumns(photos),
                        }
                    }
                }
            }
        })
        .from(feedbackFirst)
        .innerJoin(project, eq(project.prid, feedbackFirst.prid))
        .innerJoin(users, eq(users.uid, feedbackFirst.uidFrom))
        .innerJoin(userAbout, eq(userAbout.uid, users.uid))
        .innerJoin(avatars, eq(avatars.uid, users.uid))
        .innerJoin(photos, eq(photos.phid, avatars.phid))

There is an error near avatar

Object literal may only specify known properties, and avatar does not exist in type
Снимок экрана 2024-03-20 в 16 33 18

Despite of the type error, the query result is correct, with all nesting. How to fix the type error? And is it possible?

Expected behavior

The result has the correct types

Environment & setup

webstorm 2023.3.5 ts 5 node 21.4.0 macos sonoma 14.4

incredible007 avatar Mar 20 '24 21:03 incredible007

I believe its impossible to tell if this is a bug or not without seeing your drizzle schema file

JohnAllenTech avatar Mar 21 '24 00:03 JohnAllenTech


import { pgTable, pgSchema, foreignKey, pgEnum, bigint, integer, numeric, timestamp, varchar, smallint, uniqueIndex, boolean, bigserial, json, text, real, unique, index, time, date, serial, doublePrecision, primaryKey } from "drizzle-orm/pg-core"
  import { sql } from "drizzle-orm"

export const language = pgEnum("language", ['french', 'spanish', 'russian', 'english'])

export const cldata = pgSchema("cldata");

export const feedbackFirst = cldata.table("feedback_first", {
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	fid: bigint("fid", { mode: "number" }).default(sql`nextval('cldata.s_fid'::regclass)`).primaryKey().notNull(),
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	prid: bigint("prid", { mode: "number" }).notNull().references(() => project.prid),
	uidFrom: integer("uid_from").references(() => users.uid),
	aidTo: integer("aid_to").references(() => artist.aid),
	comfort: numeric("comfort"),
	focus: numeric("focus"),
	details: numeric("details"),
	responsible: numeric("responsible"),
	comment: varchar("comment", { length: 300 }),
	missed: boolean("missed").notNull(),
	date: timestamp("date", { precision: 6, withTimezone: true, mode: 'string' }),
},
(table) => {
	return {
		aidUidPrid: uniqueIndex("aid_uid_prid").on(table.prid, table.uidFrom, table.aidTo),
	}
});

export const project = cldata.table("project", {
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	prid: bigint("prid", { mode: "number" }).default(sql`nextval('cldata.s_prid'::regclass)`).primaryKey().notNull(),
	uidStarted: integer("uid_started").notNull(),
	started: timestamp("started", { precision: 6, withTimezone: true, mode: 'string' }).notNull(),
	description: varchar("description", { length: 2000 }),
	place: varchar("place", { length: 60 }),
	timesheet: varchar("timesheet", { length: 2000 }),
	resultslink: varchar("resultslink"),
	fromdate: timestamp("fromdate", { precision: 6, withTimezone: true, mode: 'string' }),
	todate: timestamp("todate", { precision: 6, withTimezone: true, mode: 'string' }),
	fullday: boolean("fullday"),
	aid: integer("aid").references(() => artist.aid),
	gone: boolean("gone").default(false),
	tfp: integer("tfp"),
},
(table) => {
	return {
		pridFromdateTodate: uniqueIndex("prid_fromdate_todate").on(table.prid, table.fromdate, table.todate, table.gone),
		ind2: uniqueIndex("project_ind2").on(table.uidStarted, table.started),
	}
});

export const artist = cldata.table("artist", {
	aid: integer("aid").default(sql`nextval('cldata.s_aid'::regclass)`).primaryKey().notNull(),
	uid: integer("uid").notNull().references(() => users.uid, { onDelete: "restrict" } ),
	atype: smallint("atype").notNull().references(() => dAtype.id, { onDelete: "restrict" } ),
	hourprice: integer("hourprice").default(0).notNull(),
	experience: smallint("experience").notNull().references(() => dExperience.id, { onDelete: "restrict" } ),
	tfp: smallint("tfp").notNull().references(() => dTfp.id, { onDelete: "restrict" } ),
	haveStudio: smallint("have_studio").default(1).notNull(),
	priceList: varchar("price_list", { length: 30000 }),
	rating: doublePrecision("rating"),
	reviews: smallint("reviews").default(0),
	companyName: varchar("company_name"),
	active: boolean("active").default(true).notNull(),
	comfort: doublePrecision("comfort"),
	focus: doublePrecision("focus"),
	details: doublePrecision("details"),
	responsible: doublePrecision("responsible"),
	quality: doublePrecision("quality"),
	quantity: doublePrecision("quantity"),
	intime: doublePrecision("intime"),
},
(table) => {
	return {
		uidAid: uniqueIndex("uid_aid").on(table.aid, table.uid),
		uidAtype: uniqueIndex("artist_uid_atype").on(table.uid, table.atype),
	}
});

export const users = cldata.table("users", {
	uid: integer("uid").default(sql`nextval('cldata.s_uid'::regclass)`).primaryKey().notNull(),
	password: varchar("password").notNull(),
	email: varchar("email", { length: 70 }).notNull(),
	regtime: timestamp("regtime", { precision: 6, withTimezone: true, mode: 'string' }).notNull(),
	authprovider: varchar("authprovider").notNull(),
	vkId: varchar("vk_id"),
	fbId: varchar("fb_id"),
	salt: varchar("salt").notNull(),
	userchannel: varchar("userchannel").notNull(),
	blocked: smallint("blocked"),
	blockedDate: date("blocked_date"),
	issup: smallint("issup"),
	deviceId: text("device_id"),
	isProfileComplete: boolean("is_profile_complete").default(true).notNull(),
},
(table) => {
	return {
		uidKey: uniqueIndex("users_uid_key").on(table.uid),
		emailUnique: unique("email_unique").on(table.email),
		userchannelUnique: unique("userchannel_unique").on(table.userchannel),
	}
});

export const prResults = cldata.table("pr_results", {
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	id: bigint("id", { mode: "number" }).default(sql`nextval('cldata.s_pr_result'::regclass)`).primaryKey().notNull(),
	uid: integer("uid"),
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	prid: bigint("prid", { mode: "number" }).references(() => project.prid, { onDelete: "cascade", onUpdate: "cascade" } ),
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	phid: bigint("phid", { mode: "number" }),
},
(table) => {
	return {
		pridPhid: uniqueIndex("prid_phid").on(table.prid, table.phid),
	}
});

export const photos = cldata.table("photos", {
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	phid: bigint("phid", { mode: "number" }).default(sql`nextval('cldata.s_phid'::regclass)`).primaryKey().notNull(),
	uid: integer("uid").notNull(),
	filename: varchar("filename").notNull(),
	date: timestamp("date", { precision: 6, withTimezone: true, mode: 'string' }).notNull(),
	incloud: boolean("incloud").default(false),
	clouduploadtime: timestamp("clouduploadtime", { withTimezone: true, mode: 'string' }),
	localdeleted: boolean("localdeleted").default(false).notNull(),
	seoFilename: varchar("seo_filename"),
	altText: varchar("alt_text"),
	caption: varchar("caption"),
	keywords: varchar("keywords"),
	author: varchar("author"),
	credentials: varchar("credentials"),
	copyright: varchar("copyright"),
	noSeo: boolean("no_seo"),
},
(table) => {
	return {
		idFilename: index("id_filename").on(table.phid, table.filename),
	}
});

export const userAbout = cldata.table("user_about", {
	uid: integer("uid").primaryKey().notNull().references(() => users.uid, { onDelete: "restrict" } ),
	firstname: varchar("firstname", { length: 30 }).notNull(),
	lastname: varchar("lastname", { length: 30 }).notNull(),
	legalentity: smallint("legalentity").references(() => dLegalentity.id),
	residence: smallint("residence").references(() => dCountry.id, { onDelete: "set null" } ),
	passportData: varchar("passport_data", { length: 100 }).default(sql`'null'::character varying`),
	enddate: timestamp("enddate", { precision: 6, withTimezone: true, mode: 'string' }),
	aboutText: varchar("about_text", { length: 2000 }),
	website: varchar("website"),
	instagram: varchar("instagram"),
	facebook: varchar("facebook"),
	vkontakte: varchar("vkontakte"),
	primaryLink: varchar("primary_link"),
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	adid: bigint("adid", { mode: "number" }).references(() => address.adid, { onDelete: "set null" } ),
	birthdate: date("birthdate"),
	defaultLang: varchar("default_lang", { length: 16 }),
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	avatarPhid: bigint("avatar_phid", { mode: "number" }),
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	signPhid: bigint("sign_phid", { mode: "number" }),
	phoneCountryCode: smallint("phone_country_code"),
	phone: varchar("phone", { length: 32 }),
	primaryNameEn: varchar("primary_name_en").notNull(),
	gender: smallint("gender").default(1).notNull().references(() => dGender.id),
	answeringTime: smallint("answering_time"),
	agreeRate: smallint("agree_rate"),
	regexpEngName: varchar("regexp_eng_name", { length: 150 }),
	phoneconfirm: boolean("phoneconfirm").default(false),
	wechat: varchar("wechat"),
	whatsapp: varchar("whatsapp"),
	youtube: varchar("youtube"),
	tiktok: varchar("tiktok"),
	specializations: integer("specializations").array(),
	lastOnline: timestamp("last_online", { mode: 'string' }),
	defaultLangId: integer("default_lang_id").references(() => dLanguages.langId),
},
(table) => {
	return {
		allUa: uniqueIndex("all_ua").on(table.uid, table.firstname, table.lastname, table.birthdate),
	}
});

export const dLegalentity = cldata.table("d_legalentity", {
	id: smallint("id").default(sql`nextval('cldata.d_legalentity_id_seq'::regclass)`).primaryKey().notNull(),
	nameEn: varchar("name_en").notNull(),
	nameRu: varchar("name_ru").notNull(),
},
(table) => {
	return {
		idKey: uniqueIndex("d_legalentity_id_key").on(table.id),
		dLegalentityNameEngKey: unique("d_legalentity_name_eng_key").on(table.nameEn),
		dLegalentityNameRusKey: unique("d_legalentity_name_rus_key").on(table.nameRu),
	}
});

export const dCountry = cldata.table("d_country", {
	nameRu: varchar("name_ru").notNull(),
	nameEn: varchar("name_en").notNull(),
	id: integer("id").primaryKey().notNull(),
	countrycode: varchar("countrycode"),
	show: boolean("show"),
	phoneCode: varchar("phone_code", { length: 255 }),
	defcurrency: smallint("defcurrency"),
},
(table) => {
	return {
		idKey: uniqueIndex("d_country_id_key").on(table.id),
	}
});

export const address = cldata.table("address", {
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	adid: bigint("adid", { mode: "number" }).default(sql`nextval('cldata.s_adid'::regclass)`).primaryKey().notNull(),
	uid: integer("uid").notNull().references(() => users.uid),
	city: integer("city").notNull().references(() => dCity.cityId),
	zipcode: varchar("zipcode"),
	exactAddr: varchar("exact_addr", { length: 100 }),
	fromDate: timestamp("from_date", { precision: 6, withTimezone: true, mode: 'string' }).default(sql`NULL::timestamp with time zone`),
	toDate: timestamp("to_date", { precision: 6, withTimezone: true, mode: 'string' }).default(sql`NULL::timestamp with time zone`),
},
(table) => {
	return {
		allInd: uniqueIndex("all_ind").on(table.adid, table.uid, table.city, table.fromDate, table.toDate),
	}
});

export const dGender = cldata.table("d_gender", {
	id: integer("id").default(sql`nextval('cldata.s_gid'::regclass)`).primaryKey().notNull(),
	nameEn: varchar("name_en").notNull(),
	nameRu: varchar("name_ru").notNull(),
},
(table) => {
	return {
		idKey: uniqueIndex("d_gender_id_key").on(table.id),
	}
});

export const dLanguages = cldata.table("d_languages", {
	langId: integer("lang_id").default(sql`nextval('cldata.d_languages_lang_id_seq'::regclass)`).primaryKey().notNull(),
	nameRu: varchar("name_ru", { length: 30 }).notNull(),
	nameEn: varchar("name_en", { length: 30 }).notNull(),
});

export const avatars = cldata.table("avatars", {
	uid: integer("uid").notNull(),
	// You can use { mode: "bigint" } if numbers are exceeding js number limitations
	phid: bigint("phid", { mode: "number" }).notNull(),
	date: timestamp("date", { withTimezone: true, mode: 'string' }),
	text: varchar("text"),
},
(table) => {
	return {
		avatarsPkey: primaryKey({ columns: [table.uid, table.phid], name: "avatars_pkey"})
	}
});

export const dAtype = cldata.table("d_atype", {
	id: smallint("id").default(sql`nextval('cldata.s_atype'::regclass)`).primaryKey().notNull(),
	nameEn: varchar("name_en").notNull(),
	nameRu: varchar("name_ru").notNull(),
	worktype: boolean("worktype").default(false),
	name: json("name"),
	description: json("description"),
},
(table) => {
	return {
		idKey: uniqueIndex("d_atype_id_key").on(table.id),
	}
});

export const dExperience = cldata.table("d_experience", {
	id: smallint("id").default(sql`nextval('cldata.d_experience_id_seq'::regclass)`).primaryKey().notNull(),
	nameEn: varchar("name_en").notNull(),
	nameRu: varchar("name_ru").notNull(),
},
(table) => {
	return {
		idKey: uniqueIndex("d_experience_id_key").on(table.id),
	}
});

export const dTfp = cldata.table("d_tfp", {
	id: smallint("id").default(sql`nextval('cldata.s_tfp'::regclass)`).primaryKey().notNull(),
	nameEn: varchar("name_en").notNull(),
	nameRu: varchar("name_ru").notNull(),
},
(table) => {
	return {
		idKey: uniqueIndex("d_tfp_id_key").on(table.id),
	}
});

export const dCity = cldata.table("d_city", {
	cityId: integer("city_id").primaryKey().notNull(),
	nameRu: varchar("name_ru", { length: 100 }),
	nameEn: varchar("name_en", { length: 100 }),
	countryId: integer("country_id").references(() => dCountry.id, { onUpdate: "cascade" } ),
	latitude: real("latitude"),
	longtitude: real("longtitude"),
	show: boolean("show"),
	population: integer("population"),
},
(table) => {
	return {
		country: uniqueIndex("d_city_country").on(table.cityId, table.countryId, table.population),
		iCityEn: uniqueIndex("i_city_en").on(table.cityId, table.nameRu, table.nameEn, table.countryId, table.latitude, table.longtitude, table.population),
		iCityRu: uniqueIndex("i_city_ru").on(table.cityId, table.nameRu, table.countryId, table.latitude, table.longtitude, table.population),
	}
});

export const dCountry = cldata.table("d_country", {
	nameRu: varchar("name_ru").notNull(),
	nameEn: varchar("name_en").notNull(),
	id: integer("id").primaryKey().notNull(),
	countrycode: varchar("countrycode"),
	show: boolean("show"),
	phoneCode: varchar("phone_code", { length: 255 }),
	defcurrency: smallint("defcurrency"),
},
(table) => {
	return {
		idKey: uniqueIndex("d_country_id_key").on(table.id),
	}
});

incredible007 avatar Mar 21 '24 01:03 incredible007

Another clarification. The only first-level nested object has no type issues

incredible007 avatar Mar 21 '24 18:03 incredible007

Your userAbout table doesnt have an avatar property? You have avatarPhid

JohnAllenTech avatar Mar 21 '24 18:03 JohnAllenTech

userAbout doesnt has an avatar prop, only avatarPhid. The avatar prop I want to have in select result. It can be any name

incredible007 avatar Mar 21 '24 18:03 incredible007

ok that makes sense apologies still getting up to speed with Postgres. But you havent let drizzle know that avatarPhid references an id on the avatar table?

JohnAllenTech avatar Mar 21 '24 18:03 JohnAllenTech

ie avatarPhid: bigint("avatar_phid").references(() => avatar.id),

JohnAllenTech avatar Mar 21 '24 18:03 JohnAllenTech

Yes, avatarPhid isnt fk. And I prefer not to use fk in this case. Thats the main reason why I broke up with prisma 🙂

incredible007 avatar Mar 21 '24 18:03 incredible007

Oh i see so you want it to dynamically generate the typescript based on the query and not the relations you have provided in the drizzle schema... jeez.. afraid I wont be any help

JohnAllenTech avatar Mar 21 '24 18:03 JohnAllenTech

Yes, exactly

incredible007 avatar Mar 21 '24 18:03 incredible007

Up for this issue. First level doesn't have type issue... Can't adding nested object inside the first level of object... The query is doing fine, though, despite the type issue.

ExRazor avatar May 17 '24 16:05 ExRazor

Still no update for this issue...

ExRazor avatar Jun 02 '24 12:06 ExRazor

Found this solution with type-preserve and without subqueries

export function jsonBuildObject<T extends SelectedFields<any, any>>(shape: T) {
    const chunks: SQL[] = []

    Object.entries(shape).forEach(([key, value]) => {
        if (chunks.length > 0) {
            chunks.push(sql.raw(`,`))
        }

        chunks.push(sql.raw(`'${key}',`))

        // json_build_object formats to ISO 8601 ...
        if (is(value, PgTimestampString)) {
            chunks.push(sql`timezone('UTC', ${value})`)
        } else {
            chunks.push(sql`${value}`)
        }
    })

    return sql<SelectResultFields<T>>`coalesce(json_build_object(${sql.join(
        chunks
    )}), '{}')`
}

        const res = await tx
            .select({
                ...getTableColumns(commPhotos),
                user: jsonBuildObject({
                    ...getTableColumns(users),
                    userAbout: jsonBuildObject({
                        ...getTableColumns(userAbout),
                        avatar: jsonBuildObject({
                            ...getTableColumns(avatars),
                            photo: jsonBuildObject({
                                ...getTableColumns(photos),
                            }),
                        }),
                    }),
                }),
            })
            .from(commPhotos)
            .leftJoin(users, eq(users.uid, commPhotos.uid))
            .leftJoin(userAbout, eq(userAbout.uid, users.uid))
            .leftJoin(avatars, eq(avatars.phid, userAbout.avatarPhid))
            .leftJoin(photos, eq(photos.phid, avatars.phid))
            .where(eq(commPhotos.commid, dto.commentId))
            .limit(1)

incredible007 avatar Jun 23 '24 09:06 incredible007

There is well known feature of typeORM that when you use select and joins (not the relational object builder), there is a leftJoinAndSelect which when used will give the nested data between one-many relation etc. Like drizzle provides with its simple findMany but the filters for joined tables are not applied on global where. So it would help if the select can also give us nested data.

m-salman-afzal avatar Nov 10 '24 02:11 m-salman-afzal

Having a look at this, as I'm in the same situation, seems to be purely a types thing, as indeed it does work as expected.

The specific types, emitted in drizzle-orm/operations.d.ts (in 0.29, not sure if that has changed, but the issue persists in 0.32.2) are:

export type SelectedFields<TColumn extends Column, TTable extends Table> = Record<string, SelectedFieldsFlat<TColumn>[string] | TTable | SelectedFieldsFlat<TColumn>>;

If we look at SelectedFieldsFlat it's:

export type SelectedFieldsFlat<TColumn extends Column> = Record<string, TColumn | SQL | SQL.Aliased>;

Which as the name tells, must be a flat record, no nesting. So maybe this is intentional because some of the db adaptors won't support nesting? I'm trying on postgres.

adlpz avatar Nov 27 '24 11:11 adlpz

same thing here! Definitely a bummer that the types don't agree, because the query also works for me, but now all my trpc code thinks it's broken. would love to see this implemented!

I asked about this in the drizzle discord too, because in my case I was initially using the column name name and it thought I was referencing the name field on PgColumn image image

ayyjohn avatar Jan 10 '25 19:01 ayyjohn

+1, it seems that's impossible to use nesting without the type errors, despite the query goes fine

eliabexp avatar Feb 12 '25 02:02 eliabexp