From 8b6d7e5651bee2c639bbd8e473267308818a5377 Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Tue, 25 Feb 2025 11:57:05 +0200 Subject: [PATCH 01/10] Add new rqb docs --- src/content/docs/_meta.json | 11 +- .../docs/relations-schema-declaration.mdx | 458 +++++++++ src/content/docs/relations-v2.mdx | 567 ++++++++++ src/content/docs/rqb-v2.mdx | 964 ++++++++++++++++++ src/content/docs/rqb.mdx | 84 +- 5 files changed, 2039 insertions(+), 45 deletions(-) create mode 100644 src/content/docs/relations-schema-declaration.mdx create mode 100644 src/content/docs/relations-v2.mdx create mode 100644 src/content/docs/rqb-v2.mdx diff --git a/src/content/docs/_meta.json b/src/content/docs/_meta.json index 02966d8c..7a4b34e6 100644 --- a/src/content/docs/_meta.json +++ b/src/content/docs/_meta.json @@ -9,8 +9,9 @@ "Fundamentals", ["sql-schema-declaration", "Schema"], + ["relations-schema-declaration", "Relations"], ["connect-overview", "Database connection"], - ["data-querying", "Query data"], + ["data-querying", "Query Data"], ["migrations", "Migrations"], "Connect::", @@ -50,9 +51,11 @@ ["sequences", "Sequences"], ["views", "Views"], ["schemas", "Schemas"], + ["relations-v2", "Drizzle Relations"], ["rls", "Row-Level Security (RLS)"], ["extensions", "Extensions"], - ["relations", "Relations"], + "---", + ["relations", "[OLD] Drizzle Relations"], "Migrations", ["kit-overview", "Overview"], @@ -76,7 +79,7 @@ ["seed-versioning", "Versioning"], "Access your data", - ["rqb", "Query"], + ["rqb-v2", "Query"], ["select", "Select"], ["insert", "Insert"], ["update", "Update"], @@ -85,6 +88,8 @@ ["query-utils", "Utils"], ["joins", "Joins"], ["sql", "Magic sql`` operator"], + "---", + ["rqb", "[OLD] Query V1"], "Performance", ["perf-queries", "Queries"], diff --git a/src/content/docs/relations-schema-declaration.mdx b/src/content/docs/relations-schema-declaration.mdx new file mode 100644 index 00000000..c263b12c --- /dev/null +++ b/src/content/docs/relations-schema-declaration.mdx @@ -0,0 +1,458 @@ +import Tab from '@mdx/Tab.astro'; +import Tabs from '@mdx/Tabs.astro'; +import Callout from '@mdx/Callout.astro'; +import SimpleLinkCards from '@mdx/SimpleLinkCards.astro'; +import CodeTabs from "@mdx/CodeTabs.astro"; +import Section from '@mdx/Section.astro'; +import Flex from "@mdx/Flex.astro" +import LinksList from "@mdx/LinksList.astro" + +# Drizzle Relations Fundamentals + +In the world of databases, especially relational databases, the concept of relations is absolutely fundamental. +Think of "relations" as the connections and links between different pieces of data. Just like in real life, +where people have relationships with each other, or objects are related to categories, databases use relations to model how different +types of information are connected and work together. + +{/* This page will guide you through the core ideas of database relations, explaining why they're so important and how they're structured. We'll cover: + +- **Database Normalization**: How to organize your data effectively. +- **Types of Relationships**: One-to-One, One-to-Many, and Many-to-Many. +- **Foreign Key Constraints**: How to enforce these relationships and keep your data accurate. +- **Polymorphic Relationships**: A glimpse into more complex connections. */} + +### Normalization +Normalization is the process of organizing data in your database to reduce redundancy (duplication) and improve data integrity +(accuracy and consistency). Think of it like tidying up a messy filing cabinet. Instead of having all sorts of papers +crammed into one folder, you organize them into logical folders and categories to make everything easier to find and manage. + + +- **Reduces Data Redundancy**: Imagine storing a customer's address every time they place an order. If the address changes, you'd have to update it in multiple places! Normalization helps you store information in one place and refer to it from other places, minimizing repetition. +- **Improves Data Integrity**: Less redundancy means less chance of inconsistencies. If you update an address in one place, it's updated everywhere it's needed. +- **Prevents Anomalies**: Normalization helps prevent issues like: + 1. **Insertion Anomalies**: Difficulty adding new data because you're missing related information. + 2. **Update Anomalies**: Having to update the same information in multiple rows. + 3. **Deletion Anomalies**: Accidentally losing valuable information when you delete something seemingly unrelated. +- **Easier to Understand and Maintain**: A normalized database is generally more logically structured and easier to understand, query, and modify. + + +Normalization is often described in terms of "normal forms" (1NF, 2NF, 3NF, and beyond). While the details can get quite technical, the core ideas are straightforward: + +#### 1NF (First Normal Form): `Atomic Values` + +**Goal**: Each column should hold a single, indivisible value. No repeating groups of data within a single cell + +**Example**: Instead of having a single `address` column that stores `123 Main St, City, USA`, you'd +break it down into separate columns: `street_address`, `city`, `state`, `zip_code`. + +```sql +-- Unnormalized (violates 1NF) +CREATE TABLE Customers_Unnormalized ( + customer_id INT PRIMARY KEY, + name VARCHAR(255), + address VARCHAR(255) -- Problem: Multiple pieces of info in one column +); + +-- Normalized to 1NF +CREATE TABLE Customers_1NF ( + customer_id INT PRIMARY KEY, + name VARCHAR(255), + street_address VARCHAR(255), + city VARCHAR(255), + state VARCHAR(255), + zip_code VARCHAR(10) +); +``` + +#### 2NF (Second Normal Form): `Eliminate Redundant Data Dependent on Part of the Key` + +**Goal**: Applies when you have a table with a composite primary key (a primary key made up of two or more columns). +2NF ensures that all non-key attributes are fully dependent on the entire composite primary key, not just part of it. + +Imagine we have a table called `order_items`. This table tracks items within orders, and we use a composite primary key (`order_id`, `product_id`) +because a single order can have multiple of the same product (though in this simplified example, let's assume each product appears only +once per order for clarity, but the composite key logic still applies). + + +```sql +CREATE TABLE OrderItems_Unnormalized ( + order_id INT, + product_id VARCHAR(10), + product_name VARCHAR(100), + product_price DECIMAL(10, 2), + quantity INT, + order_date DATE, + PRIMARY KEY (order_id, product_id) -- Composite Primary Key +); + +INSERT INTO OrderItems_Unnormalized (order_id, product_id, product_name, product_price, quantity, order_date) VALUES +(101, 'A123', 'Laptop', 1200.00, 1, '2023-10-27'), +(101, 'B456', 'Mouse', 25.00, 2, '2023-10-27'), +(102, 'A123', 'Laptop', 1200.00, 1, '2023-10-28'), +(103, 'C789', 'Keyboard', 75.00, 1, '2023-10-29'); +``` +``` ++------------------------------------------------------------------------------------+ +| OrderItems_Unnormalized | ++------------------------------------------------------------------------------------+ +| PK (order_id, product_id) | product_name | product_price | quantity | order_date | ++------------------------------------------------------------------------------------+ +| 101, A123 | Laptop | 1200.00 | 1 | 2023-10-27 | +| 101, B456 | Mouse | 25.00 | 2 | 2023-10-27 | +| 102, A123 | Laptop | 1200.00 | 1 | 2023-10-28 | +| 103, C789 | Keyboard | 75.00 | 1 | 2023-10-29 | ++------------------------------------------------------------------------------------+ +``` + + +**Problem**: Notice that `product_name` and `product_price` are repeated whenever the same `product_id` appears in different orders. +These attributes are only dependent on `product_id`, which is part of the composite primary key (`order_id`, `product_id`), but not the entire key. +This is a partial dependency. + +To achieve 2NF, we need to remove the partially dependent attributes (`product_name`, `product_price`) and place them in a separate table where they are +fully dependent on the primary key of that new table. + + +``` ++-------------------+ 1:M +---------------------------+ +| Products | <---------- | OrderItems_2NF | ++-------------------+ +---------------------------+ +| PK product_id | | PK (order_id, product_id) | +| product_name | | quantity | +| product_price | | order_date | ++-------------------+ | FK product_id | + +---------------------------+ +``` +```sql +CREATE TABLE Products ( + product_id VARCHAR(10) PRIMARY KEY, + product_name VARCHAR(100), + product_price DECIMAL(10, 2) +); + +CREATE TABLE OrderItems_2NF ( + order_id INT, + product_id VARCHAR(10), + quantity INT, + order_date DATE, + PRIMARY KEY (order_id, product_id), -- Composite Primary Key remains + FOREIGN KEY (product_id) REFERENCES Products(product_id) -- Foreign Key to Products +); + +-- Insert data into Products +INSERT INTO Products (product_id, product_name, product_price) VALUES +('A123', 'Laptop', 1200.00), +('B456', 'Mouse', 25.00), +('C789', 'Keyboard', 75.00); + +-- Insert data into OrderItems_2NF (referencing Products) +INSERT INTO OrderItems_2NF (order_id, product_id, quantity, order_date) VALUES +(101, 'A123', 1, '2023-10-27'), +(101, 'B456', 2, '2023-10-27'), +(102, 'A123', 1, '2023-10-28'), +(103, 'C789', 1, '2023-10-29'); +``` + + +#### 3NF (Third Normal Form): `Eliminate Redundant Data Dependent on Non-Key Attributes` + +**Goal**: Remove data that is dependent on other non-key attributes. This is about eliminating transitive dependencies. + +**Problem**: Let's say we have a `suppliers` table. We store supplier information, including their `zip_code`, `city`, and `state`. `supplier_id` is the primary key. + + +```sql +CREATE TABLE suppliers ( + supplier_id VARCHAR(10) PRIMARY KEY, + supplier_name VARCHAR(255), + zip_code VARCHAR(10), + city VARCHAR(100), + state VARCHAR(50) +); + +INSERT INTO suppliers (supplier_id, supplier_name, zip_code, city, state) VALUES +('S1', 'Acme Corp', '12345', 'Anytown', 'NY'), +('S2', 'Beta Inc', '67890', 'Otherville', 'CA'), +('S3', 'Gamma Ltd', '12345', 'Anytown', 'NY'); +``` +``` ++---------------------------------------------------------------+ +| suppliers | ++---------------------------------------------------------------+ +| PK supplier_id | supplier_name | zip_code | city | state | ++---------------------------------------------------------------+ +| S1 | Acme Corp | 12345 | Anytown | NY | +| S2 | Beta Inc | 67890 | Otherville | CA | +| S3 | Gamma Ltd | 12345 | Anytown | NY | ++---------------------------------------------------------------+ +``` + + +**Solution**: To achieve 3NF, we remove the attributes dependent on the non-key attribute (`city`, `state` dependent on `zip_code`) and put +them into a separate table keyed by the non-key attribute itself (`zip_code`). + + +``` ++-------------------+ 1:M +--------------------+ +| zip_codes | <---------- | suppliers | ++-------------------+ +--------------------+ +| PK zip_code | | PK supplier_id | +| city | | supplier_name | +| state | | FK zip_code | ++-------------------+ +--------------------+ +``` +```sql +CREATE TABLE zip_codes ( + zip_code VARCHAR(10) PRIMARY KEY, + city VARCHAR(100), + state VARCHAR(50) +); + +CREATE TABLE suppliers ( + supplier_id VARCHAR(10) PRIMARY KEY, + supplier_name VARCHAR(255), + zip_code VARCHAR(10), -- Foreign Key to zip_codes + FOREIGN KEY (zip_code) REFERENCES zip_codes(zip_code) +); + +-- Insert data into zip_codes +INSERT INTO zip_codes (zip_code, city, state) VALUES +('12345', 'Anytown', 'NY'), +('67890', 'Otherville', 'CA'); + +-- Insert data into suppliers (referencing zip_codes) +INSERT INTO suppliers (supplier_id, supplier_name, zip_code) VALUES +('S1', 'Acme Corp', '12345'), +('S2', 'Beta Inc', '67890'), +('S3', 'Gamma Ltd', '12345'); +``` + + +### Database Relationships + +#### One-to-One + +In a one-to-one relationship, each record in `table A` is related to at most one record in `table B`, and each record in `table B` is +related to at most one record in `table A`. It's a very direct, exclusive pairing. + + +1. **User Profiles and User Account Details**: Think of a website. Each user account (in a Users table) might have exactly one user profile (in a UserProfiles table) containing more detailed information. +2. **Employees and Parking Spaces**: An Employees table and a ParkingSpaces table. Each employee might be assigned at most one parking space, and each parking space is assigned to at most one employee. +3. **Splitting Tables for Organization**: Sometimes, you might split a very wide table into two for better organization or security reasons, maintaining a 1-1 relationship between them. + +``` +Table A (One Side) Table B (One Side) ++---------+ +---------+ +| PK (A) | <---------> | FK (A) | (Foreign Key referencing Table A) +| ... | | ... | ++---------+ +---------+ +``` + + + +#### One-to-Many + +In a one-to-many relationship, one record in `table A` can be related to many records in `table B`, but each +record in `table B` is related to at most one record in `table A`. Think of it as a "parent-child" relationship. + + +1. **Customers and Orders**: One customer can place many orders, but each order belongs to only one customer. +2. **Authors and Books**: One author can write many books, but (let's simplify for now and say) each book is written by one primary author. +3. **Departments and Employees**: One department can have many employees, but each employee belongs to only one department. + +``` +Table A (One Side) Table B (Many Side) ++---------+ +---------+ +| PK (A) | ----------> | FK (A) | (Foreign Key referencing Table A) +| ... | | ... | ++---------+ +---------+ + (One) (Many) +``` + + +#### Many-to-Many + +In a many-to-many relationship, one record in `table A` can be related to many records in `table B`, and one +record in `table B` can be related to many records in `table A`. It's a more complex, bidirectional relationship. + + +1. **Students and Courses**: One student can enroll in many courses, and one course can have many students enrolled. +2. **Products and Categories**: One product can belong to multiple categories (e.g., a "T-shirt" can be +in "Clothing" and "Summer Wear" categories), and one category can contain many products. +3. **Authors and Books**: A book can be written by multiple authors, and an author can write multiple books. + +``` +Table A (Many Side) Junction Table Table B (Many Side) ++---------+ +-------------+ +---------+ +| PK (A) | -------->| FK (A) | <----| FK (B) | +| ... | | FK (B) | | ... | ++---------+ +-------------+ +---------+ + (Many) (Junction) (Many) +``` + + +Many-to-many relationships are not directly implemented with foreign keys between the two main tables. +Instead, you need a `junction` table (also called an associative table or bridging table). +This table acts as an intermediary to link records from both tables. + +```sql +-- Table for Students (Many side) +CREATE TABLE students ( + iid INT PRIMARY KEY, + name VARCHAR(255) +); + +-- Table for Courses (Many side) +CREATE TABLE courses ( + id INT PRIMARY KEY, + name VARCHAR(255), + credits INT +); + +-- Junction Table: Enrollments (Connects Students and Courses - M-M relationship) +CREATE TABLE enrollments ( + id INT PRIMARY KEY AUTO_INCREMENT, -- Optional, but good practice for junction tables + student_id INT, + course_id INT, + enrollment_date DATE, + -- Composite Foreign Keys (often part of a composite primary key or unique constraint) + FOREIGN KEY (student_id) REFERENCES students(id), + FOREIGN KEY (course_id) REFERENCES courses(id), + UNIQUE KEY (student_id, course_id) -- Prevent duplicate enrollments for the same student and course +); +``` + +### Why Foreign Keys? + +You might think of foreign key constraints as simply a way to validate data - ensuring +that when you enter a value in a foreign key column, that value actually exists in the primary key +column of another table. And you'd be partially right! This value checking is the mechanism foreign keys use. + +But it's crucial to understand that this validation is not the end goal, it's the means +to a much larger purpose. Foreign key constraints are fundamentally about: + + + +We've discussed relationships like `One-to-Many` between Customers and Orders. +A foreign key is the SQL language's way of telling the database: + +> Hey database, I want to enforce a 1-M relationship here. Every value in the customer_id column of the +Orders table must correspond to a valid customer_id in the Customers table. + +It's not just a suggestion; it's a constraint the database actively enforces. +The database becomes relationship-aware because of the foreign key. + + + +- This is the core of "data integrity" in the context of relationships. Referential integrity means +that relationships between tables remain consistent and valid over time. +- Foreign keys prevent orphaned records. What's an orphaned record? In our Customer-Order example, +an order that exists in the Orders table but doesn't have a corresponding customer in the Customers +table would be an orphan. Foreign keys prevent this from happening (or control what happens +if you try to delete a customer with orders - via CASCADE, SET NULL, etc.). +- Why is preventing orphans important? Orphaned records break the logical structure of your data. +If you have an order without a customer, you lose crucial context. Queries become unreliable, reports + become inaccurate, and your application's logic can break down. + +**Example**: +``` +Without a foreign key, you could accidentally delete a customer from the Customers +table while their orders still exist in the Orders table. Suddenly, you have orders that point to +a customer that no longer exists! A foreign key constraint prevents this data inconsistency. +``` + + + +- Foreign keys are not just about technical enforcement; they are also a crucial part of database design documentation. +- When you see a foreign key in a database schema, it immediately tells you: +`Table 'X' is related to Table 'Y' in this way.` It's a clear visual and structural indicator of relationships. +- This makes databases easier to understand, maintain, and evolve over time. New developers can quickly +grasp how different parts of the database are connected. + + +In essence, foreign key constraints are not just about checking values; they are about: + +1. Defining the rules of your data relationships +2. Actively enforcing those rules at the database level +3. Guaranteeing data integrity and consistency within those relationships +4. Making your database more robust, reliable, and understandable + +### Why NOT Foreign Keys? + +While highly beneficial, there are some scenarios where you might reconsider or use Foreign Keys with caution. +These are typically edge cases and often involve trade-offs. + + +- **Scenario**: Extremely high-volume transactional systems (e.g., real-time logging, very high-frequency trading +platforms, massive IoT data ingestion). +- **Explanation**: Every time you insert or update data in a table with a foreign key, the database system +needs to perform checks to ensure referential integrity. In extremely high-write scenarios, these +checks can introduce a small but potentially noticeable performance overhead. + + + + + + + + +### Polymorphic Relations + +Polymorphic relationships are a more advanced concept that allows a single relationship to point to +different types of entities or tables. It's about creating more flexible and adaptable relationships when +you have different kinds of data that share some commonality. + +Imagine you have an `activities` log. An activity could be a `comment` a `like` or a `share`. +Each of these `activity` types has different details. Instead of creating separate tables and +relationships for each activity type and the things they relate to, you might use a polymorphic approach. + + +- **Comments/Reviews**: A "Comment" might be related to different types of content: articles, products, videos, etc. +Instead of having separate article_id, product_id, video_id columns in a Comments table, you can use a +polymorphic relationship. +``` ++---------------------+ +| **Comments** | ++---------------------+ +| PK comment_id | +| commentable_type | ------> [Polymorphic Relationship] +| commentable_id | --------> +| user_id | +| comment_text | +| ... | ++---------------------+ + ^ + | ++---------------------+ +---------------------+ +---------------------+ +| **Articles** | | **Products** | | **Videos** | ++---------------------+ +---------------------+ +---------------------+ +| PK article_id | | PK product_id | | PK video_id | +| ... | | ... | | ... | ++---------------------+ +---------------------+ +---------------------+ +``` +- **Notifications:** A notification could be related to a user, an order, a system event, etc. +``` ++----------------------+ +| **Notifications** | ++----------------------+ +| PK notification_id | +| notifiable_type | ------> [Polymorphic Relationship] +| notifiable_id | --------> +| user_id | +| message | +| ... | ++----------------------+ + ^ + | ++---------------------+ +---------------------+ +-----------------------+ +| **Users** | | **Orders** | | **System Events** | ++---------------------+ +---------------------+ +-----------------------+ +| PK user_id | | PK order_id | | PK event_id | +| ... | | ... | | ... | ++---------------------+ +---------------------+ +-----------------------+ +``` + + +Polymorphic relationships are more complex and are often handled at the application level or +using more advanced database features (depending on the specific database system). Standard SQL doesn't +have direct, built-in support for enforcing polymorphic foreign key constraints in the same way as regular foreign keys. \ No newline at end of file diff --git a/src/content/docs/relations-v2.mdx b/src/content/docs/relations-v2.mdx new file mode 100644 index 00000000..9c71c082 --- /dev/null +++ b/src/content/docs/relations-v2.mdx @@ -0,0 +1,567 @@ +import Tab from '@mdx/Tab.astro'; +import Tabs from '@mdx/Tabs.astro'; +import IsSupportedChipGroup from '@mdx/IsSupportedChipGroup.astro'; +import Callout from '@mdx/Callout.astro'; +import Section from '@mdx/Section.astro'; +import Prerequisites from "@mdx/Prerequisites.astro"; +import CodeTab from '@mdx/CodeTab.astro'; +import CodeTabs from '@mdx/CodeTabs.astro'; + +# Drizzle soft relations + + + - **Relations Fundamentals** - get familiar with the concepts of foreign key constraints, soft relations, database normalization, etc - [read here](/docs/rqb-fundamentals) + + +The sole purpose of Drizzle relations is to let you query your relational data in the most simple and concise way: + + +
+```ts +import { drizzle } from 'drizzle-orm/…'; +import { defineRelations } from 'drizzle-orm'; +import * as p from 'drizzle-orm/pg-core'; + +export const users = p.pgTable('users', { + id: p.integer().primaryKey(), + name: p.text().notNull() +}); + +export const posts = p.pgTable('posts', { + id: p.integer().primaryKey(), + content: p.text().notNull(), + ownerId: p.integer('owner_id'), +}); + +const relations = defineRelations({ users, posts }, (r) => ({ + posts: { + author: r.one.users({ + from: r.posts.ownerId, + to: r.users.id, + }), + } +})) + +const db = drizzle(client, { relations }); + +const result = db.query.posts.findMany({ + with: { + author: true, + }, +}); +``` +```ts +[{ + id: 10, + content: "My first post!", + author: { + id: 1, + name: "Alex" + } +}] +``` +
+
+```ts +import { drizzle } from 'drizzle-orm/…'; +import { eq } from 'drizzle-orm'; +import { posts, users } from './schema'; + +const db = drizzle(client); + +const res = await db.select() + .from(posts) + .leftJoin(users, eq(posts.ownerId, users.id)) + .orderBy(posts.id) +const mappedResult = +``` +
+
+ +### `one()` +Here is a list of all fields available for `.one()` in drizzle relations + +```ts {3-11} +const relations = defineRelations({ users, posts }, (r) => ({ + posts: { + author: r.one.users({ + from: r.posts.ownerId, + to: r.users.id, + optional: false, + alias: 'custom_name', + where: { + verified: true, + } + }), + } +})) +``` + +- `author` key is a custom key that appears in the `posts` object when using Drizzle relational queries. +- `r.one.users` defines that `author` will be a single object from the `users` table rather than an array of objects. +- `from: r.posts.ownerId` specifies the table from which we are establishing a soft relation. +In this case, the relation starts from the `ownerId` column in the `posts` table. +- `to: r.users.id` specifies the table to which we are establishing a soft relation. +In this case, the relation points to the `id` column in the `users` table. +- `optional: false` at the type level makes the `author` key in the posts object `required`. +This should be used when you are certain that this specific entity will always exist. +- `alias` is used to add a specific alias to relationships between tables. If you have multiple identical relationships between two tables, you should +differentiate them using `alias` +- `where` condition can be used for polymorphic relations. It fetches relations based on a `where` statement. +For example, in the case above, only `verified authors` will be retrieved. Learn more about polymorphic relations [here]. + +### `many()` + +Here is a list of all fields available for `.many()` in drizzle relations + +```ts {3-11} +const relations = defineRelations({ users, posts }, (r) => ({ + users: { + feed: r.many.posts({ + from: r.users.id, + to: r.posts.ownerId, + optional: false, + alias: 'custom_name', + where: { + approved: true, + } + }), + } +})) +``` + +- `feed` key is a custom key that appears in the `users` object when using Drizzle relational queries. +- `r.many.posts` defines that `feed` will be an array of objects from the `posts` table rather than just an object +- `from: r.users.id` specifies the table from which we are establishing a soft relation. +In this case, the relation starts from the `id` column in the `users` table. +- `to: r.posts.ownerId` specifies the table to which we are establishing a soft relation. +In this case, the relation points to the `ownerId` column in the `posts` table. +- `optional: false` at the type level makes the `feed` key in the posts object `required`. +This should be used when you are certain that this specific entity will always exist. +- `alias` is used to add a specific alias to relationships between tables. If you have multiple identical relationships between two tables, you should +differentiate them using `alias` +- `where` condition can be used for polymorphic relations. It fetches relations based on a `where` statement. +For example, in the case above, only `approved posts` will be retrieved. Learn more about polymorphic relations [here]. + +## --- + +### One-to-one +Drizzle ORM provides you an API to define `one-to-one` relations between tables with the `defineRelations` function. + +An example of a `one-to-one` relation between users and users, where a user can invite another (this example uses a self reference): + +```typescript copy {10-17} +import { pgTable, serial, text, boolean } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +export const users = pgTable('users', { + id: integer().primaryKey(), + name: text(), + invitedBy: integer('invited_by'), +}); + +export const relations = defineRelations({ users }, (r) => ({ + users: { + invitee: r.one.users({ + from: r.users.invitedBy, + to: r.users.id, + }) + } +})); +``` + +Another example would be a user having a profile information stored in separate table. In this case, because the foreign key is stored in the "profile_info" table, the user relation have neither fields or references. This tells Typescript that `user.profileInfo` is nullable: + +```typescript copy {15-22} +import { pgTable, serial, text, integer, jsonb } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +export const users = pgTable('users', { + id: integer().primaryKey(), + name: text(), +}); + +export const profileInfo = pgTable('profile_info', { + id: serial().primaryKey(), + userId: integer('user_id').references(() => users.id), + metadata: jsonb(), +}); + +export const relations = defineRelations({ users, profileInfo }, (r) => ({ + users: { + profileInfo: r.one.profileInfo({ + from: r.users.id, + to: r.profileInfo.userId, + }) + } +})); + +const user = await db.query.posts.findFirst({ with: { profileInfo: true } }); +//____^? type { id: number, profileInfo: { ... } | null } +``` + +### One-to-many +Drizzle ORM provides you an API to define `one-to-many` relations between tables with `defineRelations` function. + +Example of `one-to-many` relation between users and posts they've written: + +```typescript copy {15-25} +import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +export const users = pgTable('users', { + id: integer('id').primaryKey(), + name: text('name'), +}); + +export const posts = pgTable('posts', { + id: integer('id').primaryKey(), + content: text('content'), + authorId: integer('author_id'), +}); + +export const relations = defineRelations({ users, posts }, (r) => ({ + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, + users: { + posts: r.many.posts(), + }, +})); +``` + +Now lets add comments to the posts: +```typescript copy {9-14,22,27-32} +... + +export const posts = pgTable('posts', { + id: integer('id').primaryKey(), + content: text('content'), + authorId: integer('author_id'), +}); + +export const comments = pgTable("comments", { + id: integer().primaryKey(), + text: text(), + authorId: integer("author_id"), + postId: integer("post_id"), +}); + +export const relations = defineRelations({ users, posts, comments }, (r) => ({ + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + comments: r.many.comments(), + }, + users: { + posts: r.many.posts(), + }, + comments: { + post: r.one.posts({ + from: r.comments.postId, + to: r.posts.id, + }), + }, +})); +``` + + +### Many-to-many +Drizzle ORM provides you an API to define `many-to-many` relations between tables through so called `junction` or `join` tables, +they have to be explicitly defined and store associations between related tables. + +Example of `many-to-many` relation between users and groups we are using `through` to bypass junction table selection and directly select many `groups` for each `user`. +```typescript copy {27-39} +import { relations } from 'drizzle-orm'; +import { integer, pgTable, primaryKey, serial, text } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: serial('id').primaryKey(), + name: text('name'), +}); + +export const groups = pgTable('groups', { + id: serial('id').primaryKey(), + name: text('name'), +}); + +export const usersToGroups = pgTable( + 'users_to_groups', + { + userId: integer('user_id') + .notNull() + .references(() => users.id), + groupId: integer('group_id') + .notNull() + .references(() => groups.id), + }, + (t) => [primaryKey({ columns: [t.userId, t.groupId] })], +); + +export const relations = defineRelations({ users, groups, usersToGroups }, + (r) => ({ + users: { + groups: r.many.groups({ + from: r.users.id.through(r.usersToGroups.userId), + to: r.groups.id.through(r.usersToGroups.groupId), + }), + }, + groups: { + participants: r.many.users(), + }, + }) +); +``` + +**Query example:** +```ts +const res = await db.query.users.findMany({ + with: { + groups: true + }, +}); + +// response type +type Response = { + id: number; + name: string | null; + groups: { + id: number; + name: string | null; + }[]; +}[]; +``` + + +Previously, you would need to query through a `junction` table and then map it out for every response + +❌ You don't need to do it now! +```ts +const response = await db._query.users.findMany({ + with: { + usersToGroups: { + columns: {}, + with: { + groups: true, + }, + }, + }, +}); + +// response type +type Response = { + id: number; + name: string | null; + usersToGroups: { + groups: { + id: number; + name: string | null; + } + }[]; +}[]; +``` + + +### Predefined filters + +Predefined `where` statements in Drizzle's relation definitions are a type of polymorphic relations implementation, but it's not fully it. Essentially, they allow you to +connect tables not only by selecting specific columns but also through custom `where` statements. Let's look at some examples: + +We can define a relation between `groups` and `users` so that when querying group's users, we only retrieve those whose `verified` column is set to `true` + +
+```ts +import { defineRelations } from "drizzle-orm"; +import * as p from "drizzle-orm/pg-core"; +import * as schema from './schema'; + +export const relations = defineRelations(schema,(r) => ({ + groups: { + verifiedUsers: r.many.users({ + from: r.groups.id.through(r.usersToGroups.groupId), + to: r.users.id.through(r.usersToGroups.userId), + where: { + verified: true, + }, + }), + }, + }) +); + +... + +await db.query.groups.findMany({ + with: { + verifiedUsers: true, + }, +}); +``` +
+
+```ts +import { defineRelations } from "drizzle-orm"; +import * as p from "drizzle-orm/pg-core"; + +export const users = p.pgTable("users", { + id: p.integer().primaryKey(), + name: p.text().notNull(), + verified: p.boolean().notNull(), +}); + +export const groups = p.pgTable("groups", { + id: p.integer().primaryKey(), + title: p.text().notNull(), +}); + +export const usersToGroups = p.pgTable( + "users_to_groups", + { + userId: p + .integer("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + groupId: p + .integer("group_id") + .notNull() + .references(() => groups.id, { onDelete: "cascade" }), + }, + (t) => [p.primaryKey({ columns: [t.groupId, t.userId] })] +); +``` +
+
+ + +You can only specify filters on the target (to) table. So in this example, the where clause will only include columns from the `users` table since we are establishing a relation **TO** users + +```ts {7} +export const relations = defineRelations(schema,(r) => ({ + groups: { + verifiedUsers: r.many.users({ + from: r.groups.id.through(r.usersToGroups.groupId), + to: r.users.id.through(r.usersToGroups.userId), + where: { + verified: true, + }, + }), + }, + }) +); +``` + + +## --- + +### Foreign keys + +You might've noticed that `relations` look similar to foreign keys — they even have a `references` property. So what's the difference? + +While foreign keys serve a similar purpose, defining relations between tables, they work on a different level compared to `relations`. + +Foreign keys are a database level constraint, they are checked on every `insert`/`update`/`delete` operation and throw an error if a constraint is violated. +On the other hand, `relations` are a higher level abstraction, they are used to define relations between tables on the application level only. +They do not affect the database schema in any way and do not create foreign keys implicitly. + +What this means is `relations` and foreign keys can be used together, but they are not dependent on each other. +You can define `relations` without using foreign keys (and vice versa), which allows them to be used with databases that do not support foreign keys. + +The following two examples will work exactly the same in terms of querying the data using Drizzle relational queries. + + +```ts {15} +export const users = pgTable('users', { + id: serial('id').primaryKey(), + name: text('name'), +}); + +export const usersRelations = relations(users, ({ one, many }) => ({ + profileInfo: one(users, { + fields: [profileInfo.userId], + references: [users.id], + }), +})); + +export const profileInfo = pgTable('profile_info', { + id: serial('id').primaryKey(), + userId: integer("user_id"), + metadata: jsonb("metadata"), +}); +``` + + +```ts {15} +export const users = pgTable('users', { + id: serial('id').primaryKey(), + name: text('name'), +}); + +export const usersRelations = relations(users, ({ one, many }) => ({ + profileInfo: one(users, { + fields: [profileInfo.userId], + references: [users.id], + }), +})); + +export const profileInfo = pgTable('profile_info', { + id: serial('id').primaryKey(), + userId: integer("user_id").references(() => users.id), + metadata: jsonb("metadata"), +}); +``` + + + +### Disambiguating relations + +Drizzle also provides the `alias` option as a way to disambiguate +relations when you define multiple of them between the same two tables. For +example, if you define a `posts` table that has the `author` and `reviewer` +relations. + +```ts {19,22,29,34} +import { pgTable, integer, text } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +export const users = pgTable('users', { + id: integer('id').primaryKey(), + name: text('name'), +}); + +export const posts = pgTable('posts', { + id: integer('id').primaryKey(), + content: text('content'), + authorId: integer('author_id'), + reviewerId: integer('reviewer_id'), +}); + +export const relations = defineRelations({ users, posts }, (r) => ({ + users: { + posts: r.many.posts({ + alias: "author", + }), + reviewedPosts: r.many.posts({ + alias: "reviewer", + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + alias: "author", + }), + reviewer: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + alias: "reviewer", + }), + }, +})); +``` + +### Troubleshooting + diff --git a/src/content/docs/rqb-v2.mdx b/src/content/docs/rqb-v2.mdx new file mode 100644 index 00000000..83603c26 --- /dev/null +++ b/src/content/docs/rqb-v2.mdx @@ -0,0 +1,964 @@ +import Tab from '@mdx/Tab.astro'; +import Tabs from '@mdx/Tabs.astro'; +import Callout from '@mdx/Callout.astro'; +import CodeTabs from '@mdx/CodeTabs.astro'; +import CodeTab from '@mdx/CodeTab.astro'; +import Section from '@mdx/Section.astro'; +import IsSupportedChipGroup from '@mdx/IsSupportedChipGroup.astro'; + +# Drizzle Queries + + + +Drizzle ORM is designed to be a thin typed layer on top of SQL. +We truly believe we've designed the best way to operate an SQL database from TypeScript and it's time to make it better. + +Relational queries are meant to provide you with a great developer experience for querying +nested relational data from an SQL database, avoiding multiple joins and complex data mappings. + +It is an extension to the existing schema definition and query builder. +You can opt-in to use it based on your needs. +We've made sure you have both the best-in-class developer experience and performance. + + + + ```typescript copy /schema/3 + import { relations } from './schema'; + import { drizzle } from 'drizzle-orm/...'; + + const db = drizzle({ relations }); + + const result = await db.query.users.findMany({ + with: { + posts: true + }, + }); + ``` + + ```ts + [{ + id: 10, + name: "Dan", + posts: [ + { + id: 1, + content: "SQL is awesome", + authorId: 10, + }, + { + id: 2, + content: "But check relational queries", + authorId: 10, + } + ] + }] + ``` + + + ```typescript {15-25} copy + import { defineRelations } from "drizzle-orm"; + import * as p from "drizzle-orm/pg-core"; + + export const posts = p.pgTable("posts", { + id: p.integer().primaryKey(), + content: p.text().notNull(), + authorId: p.integer("author_id").notNull(), + }); + + export const users = p.pgTable("users", { + id: p.integer().primaryKey(), + name: p.text().notNull(), + }); + + export const relations = defineRelations({ users, posts }, (r) => ({ + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, + users: { + posts: r.many.users(), + }, + })); + ``` + + +## Querying +Relational queries are an extension to Drizzle's original **[query builder](/docs/select)**. +You need to provide all `tables` and `relations` from your schema file/files upon `drizzle()` +initialization and then just use the `db.query` API. + + `drizzle` import path depends on the **[database driver](/docs/connect-overview)** you're using. + + + +```ts +import { relations } from './relations'; +import { drizzle } from 'drizzle-orm/...'; + +const db = drizzle({ relations }); + +await db.query.users.findMany(...); +``` + +```typescript copy +import { type AnyPgColumn, boolean, integer, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: integer().primaryKey(), + name: text().notNull(), + invitedBy: integer('invited_by').references((): AnyPgColumn => users.id), +}); + +export const groups = pgTable('groups', { + id: integer().primaryKey(), + name: text().notNull(), + description: text(), +}); + +export const usersToGroups = pgTable('users_to_groups', { + id: integer().primaryKey(), + userId: integer('user_id').notNull().references(() => users.id), + groupId: integer('group_id').notNull().references(() => groups.id), +}, (t) => [ + primaryKey(t.userId, t.groupId) +]); + +export const posts = pgTable('posts', { + id: integer().primaryKey(), + content: text().notNull(), + authorId: integer('author_id').references(() => users.id), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const comments = pgTable('comments', { + id: integer().primaryKey(), + content: text().notNull(), + creator: integer().references(() => users.id), + postId: integer('post_id').references(() => posts.id), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const commentLikes = pgTable('comment_likes', { + id: integer().primaryKey(), + creator: integer().references(() => users.id), + commentId: integer('comment_id').references(() => comments.id), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); +``` +```typescript copy +import { defineRelations } from 'drizzle-orm'; +import * as schema from './schema'; + +export const relations = defineRelations(schema, (r) => ({ + users: { + invitee: r.one.users({ + from: r.users.invitedBy, + to: r.users.id, + }), + groups: r.many.groups({ + from: r.users.id.through(r.usersToGroups.userId), + to: r.groups.id.through(r.usersToGroups.groupId), + }), + posts: r.many.posts(), + }, + groups: { + users: r.many.users(), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + comments: r.many.comments(), + }, + comments: { + post: r.one.posts({ + from: r.comments.postId, + to: r.posts.id, + }), + author: r.one.users({ + from: r.comments.creator, + to: r.users.id, + }), + likes: r.many.commentLikes(), + }, + commentLikes: { + comment: r.one.comments({ + from: r.commentLikes.commentId, + to: r.comments.id, + }), + author: r.one.users({ + from: r.commentLikes.creator, + to: r.users.id, + }), + }, + }) +); +``` + + +Drizzle provides `.findMany()` and `.findFirst()` APIs. +### Find many +
+```typescript copy +const users = await db.query.users.findMany(); +``` +```ts +// result type +const result: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; +}[]; +``` +
+ +### Find first + + `.findFirst()` will add `limit 1` to the query. + +
+```typescript copy +const user = await db.query.users.findFirst(); +``` +```ts +// result type +const result: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; +}; +``` +
+ +### Include relations + +`With` operator lets you combine data from multiple related tables and properly aggregate results. + +**Getting all posts with comments:** +```typescript copy +const posts = await db.query.posts.findMany({ + with: { + comments: true, + }, +}); +``` + +**Getting first post with comments:** +```typescript copy +const post = await db.query.posts.findFirst({ + with: { + comments: true, + }, +}); +``` + +You can chain nested with statements as much as necessary. +For any nested `with` queries Drizzle will infer types using [Core Type API](/docs/goodies#type-api). + +**Get all users with posts. Each post should contain a list of comments:** +```typescript copy +const users = await db.query.users.findMany({ + with: { + posts: { + with: { + comments: true, + }, + }, + }, +}); +``` + +### Partial fields select +`columns` parameter lets you include or omit columns you want to get from the database. + + + Drizzle performs partial selects on the query level, no additional data is transferred from the database. + + Keep in mind that **a single SQL statement is outputted by Drizzle.** + + +**Get all posts with just `id`, `content` and include `comments`:** +```typescript copy +const posts = await db.query.posts.findMany({ + columns: { + id: true, + content: true, + }, + with: { + comments: true, + } +}); +``` + +**Get all posts without `content`:** +```typescript copy +const posts = await db.query.posts.findMany({ + columns: { + content: false, + }, +}); +``` + + +When both `true` and `false` select options are present, all `false` options are ignored. + + +If you include the `name` field and exclude the `id` field, `id` exclusion will be redundant, +all fields apart from `name` would be excluded anyways. + +**Exclude and Include fields in the same query:** +
+```typescript copy +const users = await db.query.users.findMany({ + columns: { + name: true, + id: false //ignored + }, +}); +``` +```ts +// result type +const users: { + name: string; +}; +``` +
+ +**Only include columns from nested relations:** +
+```typescript copy +const res = await db.query.users.findMany({ + columns: {}, + with: { + posts: true + } +}); +``` +```ts +// result type +const res: { + posts: { + id: number, + text: string + } +}[]; +``` +
+ +### Nested partial fields select +Just like with **[`partial select`](#partial-select)**, you can include or exclude columns of nested relations: +```typescript copy +const posts = await db.query.posts.findMany({ + columns: { + id: true, + content: true, + }, + with: { + comments: { + columns: { + authorId: false + } + } + } +}); +``` + +### Select filters +Just like in our SQL-like query builder, +relational queries API lets you define filters and conditions with the list of our **[`operators`](/docs/operators)**. + +You can either import them from `drizzle-orm` or use from the callback syntax: +
+```typescript copy +const users = await db.query.users.findMany({ + where: { + id: 1 + } +}); +``` +```ts copy +const users = await db.query.users.findMany({ + where: { + id: 1 + } +}); +``` +
+ +Find post with `id=1` and comments that were created before particular date: +```typescript copy +await db.query.posts.findMany({ + where: { + id: 1, + }, + with: { + comments: { + where: { + createdAt: { lt: new Date() }, + }, + }, + }, + }); +``` + +**List of all filter operators** +```ts +where: { + OR: [], + NOT: {}, + RAW: (table) => sql`${table.id} = 1`, + + // `id` - is a column key + id: { + eq: 1, + ne: 1, + gt: 1, + gte: 1, + lt: 1, + lte: 1, + in: [1], + notIn: [1], + like: "", + ilike: "", + notLike: "", + notIlike: "", + isNull: true, + isNotNull: true, + }, +}, +``` + +**Examples** + + +```ts +const response = db.query.users.findMany({ + where: { + age: 15, + }, +}); +``` +```sql {3} +select "users"."id" as "id", "users"."name" as "name" +from "users" +where ("users"."age" = $1) +``` + + +```ts +const response = db.query.users.findMany({ + where: { + age: 15, + name: 'John' + }, +}); +``` +```sql {3} +select "users"."id" as "id", "users"."name" as "name" +from "users" +where ("users"."age" = $1 and "users"."name" = $2) +``` + + +```ts +const response = await db.query.users.findMany({ + where: { + OR: [ + { + id: { + gt: 10, + }, + }, + { + name: { + like: "John%", + }, + } + ], + }, +}); +``` +```sql {3} +select "users"."id" as "id", "users"."name" as "name" +from "users" +where ("users"."id" > $1 or "users"."name" like $2) +``` + + +```ts +const response = db.query.users.findMany({ + where: { + NOT: { + id: { + gt: 10, + }, + }, + name: { + like: "John%", + }, + }, +}); +``` +```sql {3} +select "users"."id" as "id", "users"."name" as "name" +from "users" +where (not "users"."id" > $1 and "users"."name" like $2) +``` + + +```ts +// schema.ts +import { integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: integer("id").primaryKey(), + name: text("name"), + email: text("email").notNull(), + age: integer("age"), + createdAt: timestamp("created_at").defaultNow(), + lastLogin: timestamp("last_login"), + subscriptionEnd: timestamp("subscription_end"), + lastActivity: timestamp("last_activity"), + preferences: jsonb("preferences"), // JSON column for user settings/preferences + interests: text("interests").array(), // Array column for user interests +}); +``` +```ts +const response = db.query.users.findMany({ + where: { + AND: [ + { + OR: [ + { RAW: (table) => sql`LOWER(${table.name}) LIKE 'john%'` }, + { name: { ilike: "jane%" } }, + ], + }, + { + OR: [ + { RAW: (table) => sql`${table.preferences}->>'theme' = 'dark'` }, + { RAW: (table) => sql`${table.preferences}->>'theme' IS NULL` }, + ], + }, + { RAW: (table) => sql`${table.age} BETWEEN 25 AND 35` }, + ], + }, +}); +``` +```sql +``` + + + +### Relations Filters + +// TODO + +### Limit & Offset +Drizzle ORM provides `limit` & `offset` API for queries and for the nested entities. + +**Find 5 posts:** +```typescript copy +await db.query.posts.findMany({ + limit: 5, +}); +``` + +**Find posts and get 3 comments at most:** +```typescript copy +await db.query.posts.findMany({ + with: { + comments: { + limit: 3, + }, + }, +}); +``` + + + `offset` is now can be used in with tables as well! + +```typescript +await db.query.posts.findMany({ + limit: 5, + offset: 2, // correct ✅ + with: { + comments: { + offset: 3, // correct ✅ + limit: 3, + }, + }, +}); +``` + +Find posts with comments from the 5th to the 10th post: +```typescript copy +await db.query.posts.findMany({ + with: { + comments: true, + }, + limit: 5, + offset: 5, +}); +``` + +### Order By +Drizzle provides API for ordering in the relational query builder. + +You can use same ordering **[core API](/docs/select#order-by)** or use +`order by` operator from the callback with no imports. + +
+```typescript copy +await db.query.posts.findMany({ + orderBy: { + id: "asc", + }, +}); +``` +
+ +**Order by `asc` + `desc`:** +```typescript copy + await db.query.posts.findMany({ + orderBy: { id: "asc" }, + with: { + comments: { + orderBy: { id: "desc" }, + }, + }, + }); +``` + +### Include custom fields +Relational query API lets you add custom additional fields. +It's useful when you need to retrieve data and apply additional functions to it. + + As of now aggregations are not supported in `extras`, please use **[`core queries`](/docs/select)** for that. + + +
+```typescript copy {5} +import { sql } from 'drizzle-orm'; + +await db.query.users.findMany({ + extras: { + loweredName: sql`lower(${users.name})`.as('lowered_name'), + }, +}) +``` +```typescript copy {3} +await db.query.users.findMany({ + extras: { + loweredName: (users, { sql }) => sql`lower(${users.name})`.as('lowered_name'), + }, +}) +``` +
+ +`lowerName` as a key will be included to all fields in returned object. + + + You have to explicitly specify `.as("")` + + +To retrieve all users with groups, but with the fullName field included (which is a concatenation of firstName and lastName), +you can use the following query with the Drizzle relational query builder. + +
+```typescript copy +const res = await db.query.users.findMany({ + extras: { + fullName: sql`concat(${users.name}, " ", ${users.name})`.as('full_name'), + }, + with: { + usersToGroups: { + with: { + group: true, + }, + }, + }, +}); +``` +```ts +// result type +const res: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + fullName: string; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; +}[]; + +``` +
+ + +To retrieve all posts with comments and add an additional field to calculate the size of the post content and the size of each comment content: +
+```typescript copy +const res = await db.query.posts.findMany({ + extras: (table, { sql }) => ({ + contentLength: (sql`length(${table.content})`).as('content_length'), + }), + with: { + comments: { + extras: { + commentSize: sql`length(${comments.content})`.as('comment_size'), + }, + }, + }, +}); +``` +```ts +// result type +const res: { + id: number; + createdAt: Date; + content: string; + authorId: number | null; + contentLength: number; + comments: { + id: number; + createdAt: Date; + content: string; + creator: number | null; + postId: number | null; + commentSize: number; + }[]; +}; +``` +
+ +### Prepared statements +Prepared statements are designed to massively improve query performance — [see here.](/docs/perf-queries) + +In this section, you can learn how to define placeholders and execute prepared statements +using the Drizzle relational query builder. + +##### **Placeholder in `where`** + + +
+```ts copy +const prepared = db.query.users.findMany({ + where: { id: { eq: sql.placeholder("id") } }, + with: { + posts: { + where: { id: 1 }, + }, + }, +}).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ id: 1 }); +``` +
+
+ +
+```ts copy +const prepared = db.query.users.findMany({ + where: { id: { eq: sql.placeholder("id") } }, + with: { + posts: { + where: { id: 1 }, + }, + }, +}).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ id: 1 }); +``` +
+
+ +
+```ts copy +const prepared = db.query.users.findMany({ + where: { id: { eq: sql.placeholder("id") } }, + with: { + posts: { + where: { id: 1 }, + }, + }, +}).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ id: 1 }); +``` +
+
+
+ + +##### **Placeholder in `limit`** + + +
+```ts copy +const prepared = db.query.users.findMany({ + with: { + posts: { + limit: sql.placeholder("limit"), + }, + }, + }).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ limit: 1 }); +``` +
+
+ +
+```ts copy +const prepared = db.query.users.findMany({ + with: { + posts: { + limit: sql.placeholder("limit"), + }, + }, + }).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ limit: 1 }); +``` +
+
+ +
+```ts copy +const prepared = db.query.users.findMany({ + with: { + posts: { + limit: sql.placeholder("limit"), + }, + }, + }).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ limit: 1 }); +``` +
+
+
+ + +##### **Placeholder in `offset`** + + +
+```ts copy +const prepared = db.query.users.findMany({ + offset: sql.placeholder('offset'), + with: { + posts: true, + }, +}).prepare('query_name'); + +const usersWithPosts = await prepared.execute({ offset: 1 }); +``` +
+
+ +
+```ts copy +const prepared = db.query.users.findMany({ + offset: sql.placeholder('offset'), + with: { + posts: true, + }, +}).prepare(); + +const usersWithPosts = await prepared.execute({ offset: 1 }); +``` +
+
+ +
+```ts copy +const prepared = db.query.users.findMany({ + offset: sql.placeholder('offset'), + with: { + posts: true, + }, +}).prepare(); + +const usersWithPosts = await prepared.execute({ offset: 1 }); +``` +
+
+
+ +##### **Multiple placeholders** + + +
+```ts copy +const prepared = db.query.users.findMany({ + limit: sql.placeholder("uLimit"), + offset: sql.placeholder("uOffset"), + where: { + OR: [{ id: { eq: sql.placeholder("id") } }, { id: 3 }], + }, + with: { + posts: { + where: { id: { eq: sql.placeholder("pid") } }, + limit: sql.placeholder("pLimit"), + }, + }, +}).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6 }); +``` +
+
+ +
+```ts copy +const prepared = db.query.users.findMany({ + limit: sql.placeholder("uLimit"), + offset: sql.placeholder("uOffset"), + where: { + OR: [{ id: { eq: sql.placeholder("id") } }, { id: 3 }], + }, + with: { + posts: { + where: { id: { eq: sql.placeholder("pid") } }, + limit: sql.placeholder("pLimit"), + }, + }, +}).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6 }); +``` +
+
+ +
+```ts copy +const prepared = db.query.users.findMany({ + limit: sql.placeholder("uLimit"), + offset: sql.placeholder("uOffset"), + where: { + OR: [{ id: { eq: sql.placeholder("id") } }, { id: 3 }], + }, + with: { + posts: { + where: { id: { eq: sql.placeholder("pid") } }, + limit: sql.placeholder("pLimit"), + }, + }, +}).prepare("query_name"); + +const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6 }); +``` +
+
+
diff --git a/src/content/docs/rqb.mdx b/src/content/docs/rqb.mdx index 01cfa093..5bc5bf53 100644 --- a/src/content/docs/rqb.mdx +++ b/src/content/docs/rqb.mdx @@ -8,7 +8,7 @@ import IsSupportedChipGroup from '@mdx/IsSupportedChipGroup.astro'; # Drizzle Queries - + Drizzle ORM is designed to be a thin typed layer on top of SQL. We truly believe we've designed the best way to operate an SQL database from TypeScript and it's time to make it better. @@ -28,7 +28,7 @@ We've made sure you have both the best-in-class developer experience and perform const db = drizzle({ schema }); - const result = await db.query.users.findMany({ + const result = await db._query.users.findMany({ with: { posts: true }, @@ -89,7 +89,7 @@ We've made sure you have both the best-in-class developer experience and perform const db = drizzle({ schema: { ...schema1, ...schema2 } }); - const result = await db.query.users.findMany({ + const result = await db._query.users.findMany({ with: { posts: true }, @@ -129,7 +129,7 @@ const db = drizzle({ client: connection, schema, mode: 'planetscale' }); ## Querying Relational queries are an extension to Drizzle's original **[query builder](/docs/select)**. You need to provide all `tables` and `relations` from your schema file/files upon `drizzle()` -initialization and then just use the `db.query` API. +initialization and then just use the `db._query` API. `drizzle` import path depends on the **[database driver](/docs/connect-overview)** you're using. @@ -141,7 +141,7 @@ import { drizzle } from 'drizzle-orm/...'; const db = drizzle({ schema }); -await db.query.users.findMany(...); +await db._query.users.findMany(...); ``` ```ts // if you have schema in multiple files @@ -151,7 +151,7 @@ import { drizzle } from 'drizzle-orm/...'; const db = drizzle({ schema: { ...schema1, ...schema2 } }); -await db.query.users.findMany(...); +await db._query.users.findMany(...); ``` ```typescript copy @@ -238,7 +238,7 @@ Drizzle provides `.findMany()` and `.findFirst()` APIs. ### Find many
```typescript copy -const users = await db.query.users.findMany(); +const users = await db._query.users.findMany(); ``` ```ts // result type @@ -257,7 +257,7 @@ const result: {
```typescript copy -const user = await db.query.users.findFirst(); +const user = await db._query.users.findFirst(); ``` ```ts // result type @@ -276,7 +276,7 @@ const result: { **Getting all posts with comments:** ```typescript copy -const posts = await db.query.posts.findMany({ +const posts = await db._query.posts.findMany({ with: { comments: true, }, @@ -285,7 +285,7 @@ const posts = await db.query.posts.findMany({ **Getting first post with comments:** ```typescript copy -const post = await db.query.posts.findFirst({ +const post = await db._query.posts.findFirst({ with: { comments: true, }, @@ -297,7 +297,7 @@ For any nested `with` queries Drizzle will infer types using [Core Type API](/do **Get all users with posts. Each post should contain a list of comments:** ```typescript copy -const users = await db.query.users.findMany({ +const users = await db._query.users.findMany({ with: { posts: { with: { @@ -319,7 +319,7 @@ const users = await db.query.users.findMany({ **Get all posts with just `id`, `content` and include `comments`:** ```typescript copy -const posts = await db.query.posts.findMany({ +const posts = await db._query.posts.findMany({ columns: { id: true, content: true, @@ -332,7 +332,7 @@ const posts = await db.query.posts.findMany({ **Get all posts without `content`:** ```typescript copy -const posts = await db.query.posts.findMany({ +const posts = await db._query.posts.findMany({ columns: { content: false, }, @@ -349,7 +349,7 @@ all fields apart from `name` would be excluded anyways. **Exclude and Include fields in the same query:**
```typescript copy -const users = await db.query.users.findMany({ +const users = await db._query.users.findMany({ columns: { name: true, id: false //ignored @@ -367,7 +367,7 @@ const users: { **Only include columns from nested relations:**
```typescript copy -const res = await db.query.users.findMany({ +const res = await db._query.users.findMany({ columns: {}, with: { posts: true @@ -388,7 +388,7 @@ const res: { ### Nested partial fields select Just like with **[`partial select`](#partial-select)**, you can include or exclude columns of nested relations: ```typescript copy -const posts = await db.query.posts.findMany({ +const posts = await db._query.posts.findMany({ columns: { id: true, content: true, @@ -412,12 +412,12 @@ You can either import them from `drizzle-orm` or use from the callback syntax: ```typescript copy import { eq } from 'drizzle-orm'; -const users = await db.query.users.findMany({ +const users = await db._query.users.findMany({ where: eq(users.id, 1) }) ``` ```ts copy -const users = await db.query.users.findMany({ +const users = await db._query.users.findMany({ where: (users, { eq }) => eq(users.id, 1), }) ``` @@ -425,7 +425,7 @@ const users = await db.query.users.findMany({ Find post with `id=1` and comments that were created before particular date: ```typescript copy -await db.query.posts.findMany({ +await db._query.posts.findMany({ where: (posts, { eq }) => (eq(posts.id, 1)), with: { comments: { @@ -440,14 +440,14 @@ Drizzle ORM provides `limit` & `offset` API for queries and for the nested entit **Find 5 posts:** ```typescript copy -await db.query.posts.findMany({ +await db._query.posts.findMany({ limit: 5, }); ``` **Find posts and get 3 comments at most:** ```typescript copy -await db.query.posts.findMany({ +await db._query.posts.findMany({ with: { comments: { limit: 3, @@ -460,7 +460,7 @@ await db.query.posts.findMany({ `offset` is only available for top level query. ```typescript -await db.query.posts.findMany({ +await db._query.posts.findMany({ limit: 5, offset: 2, // correct ✅ with: { @@ -474,7 +474,7 @@ await db.query.posts.findMany({ Find posts with comments from the 5th to the 10th post: ```typescript copy -await db.query.posts.findMany({ +await db._query.posts.findMany({ limit: 5, offset: 5, with: { @@ -493,12 +493,12 @@ You can use same ordering **[core API](/docs/select#order-by)** or use ```typescript copy import { desc, asc } from 'drizzle-orm'; -await db.query.posts.findMany({ +await db._query.posts.findMany({ orderBy: [asc(posts.id)], }); ``` ```typescript copy -await db.query.posts.findMany({ +await db._query.posts.findMany({ orderBy: (posts, { asc }) => [asc(posts.id)], }); ``` @@ -506,7 +506,7 @@ await db.query.posts.findMany({ **Order by `asc` + `desc`:** ```typescript copy -await db.query.posts.findMany({ +await db._query.posts.findMany({ orderBy: (posts, { asc }) => [asc(posts.id)], with: { comments: { @@ -527,14 +527,14 @@ It's useful when you need to retrieve data and apply additional functions to it. ```typescript copy {5} import { sql } from 'drizzle-orm'; -await db.query.users.findMany({ +await db._query.users.findMany({ extras: { loweredName: sql`lower(${users.name})`.as('lowered_name'), }, }) ``` ```typescript copy {3} -await db.query.users.findMany({ +await db._query.users.findMany({ extras: { loweredName: (users, { sql }) => sql`lower(${users.name})`.as('lowered_name'), }, @@ -553,7 +553,7 @@ you can use the following query with the Drizzle relational query builder.
```typescript copy -const res = await db.query.users.findMany({ +const res = await db._query.users.findMany({ extras: { fullName: sql`concat(${users.name}, " ", ${users.name})`.as('full_name'), }, @@ -590,7 +590,7 @@ const res: { To retrieve all posts with comments and add an additional field to calculate the size of the post content and the size of each comment content:
```typescript copy -const res = await db.query.posts.findMany({ +const res = await db._query.posts.findMany({ extras: (table, { sql }) => ({ contentLength: (sql`length(${table.content})`).as('content_length'), }), @@ -634,7 +634,7 @@ using the Drizzle relational query builder.
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ where: ((users, { eq }) => eq(users.id, placeholder('id'))), with: { posts: { @@ -650,7 +650,7 @@ const usersWithPosts = await prepared.execute({ id: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ where: ((users, { eq }) => eq(users.id, placeholder('id'))), with: { posts: { @@ -666,7 +666,7 @@ const usersWithPosts = await prepared.execute({ id: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ where: ((users, { eq }) => eq(users.id, placeholder('id'))), with: { posts: { @@ -687,7 +687,7 @@ const usersWithPosts = await prepared.execute({ id: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ with: { posts: { limit: placeholder('limit'), @@ -702,7 +702,7 @@ const usersWithPosts = await prepared.execute({ limit: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ with: { posts: { limit: placeholder('limit'), @@ -717,7 +717,7 @@ const usersWithPosts = await prepared.execute({ limit: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ with: { posts: { limit: placeholder('limit'), @@ -737,7 +737,7 @@ const usersWithPosts = await prepared.execute({ limit: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ offset: placeholder('offset'), with: { posts: true, @@ -751,7 +751,7 @@ const usersWithPosts = await prepared.execute({ offset: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ offset: placeholder('offset'), with: { posts: true, @@ -765,7 +765,7 @@ const usersWithPosts = await prepared.execute({ offset: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ offset: placeholder('offset'), with: { posts: true, @@ -783,7 +783,7 @@ const usersWithPosts = await prepared.execute({ offset: 1 });
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ limit: placeholder('uLimit'), offset: placeholder('uOffset'), where: ((users, { eq, or }) => or(eq(users.id, placeholder('id')), eq(users.id, 3))), @@ -802,7 +802,7 @@ const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ limit: placeholder('uLimit'), offset: placeholder('uOffset'), where: ((users, { eq, or }) => or(eq(users.id, placeholder('id')), eq(users.id, 3))), @@ -821,7 +821,7 @@ const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1
```ts copy -const prepared = db.query.users.findMany({ +const prepared = db._query.users.findMany({ limit: placeholder('uLimit'), offset: placeholder('uOffset'), where: ((users, { eq, or }) => or(eq(users.id, placeholder('id')), eq(users.id, 3))), From 6006e6041e56795687bbbdb299d22d66670f72ab Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Wed, 26 Feb 2025 11:13:30 +0200 Subject: [PATCH 02/10] Add relations part --- .../docs/relations-schema-declaration.mdx | 6 +++++- src/content/docs/rqb-v2.mdx | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/content/docs/relations-schema-declaration.mdx b/src/content/docs/relations-schema-declaration.mdx index c263b12c..ddd8b044 100644 --- a/src/content/docs/relations-schema-declaration.mdx +++ b/src/content/docs/relations-schema-declaration.mdx @@ -228,8 +228,12 @@ INSERT INTO suppliers (supplier_id, supplier_name, zip_code) VALUES ``` -### Database Relationships + +There are additional normal forms, such as `4NF`, `5NF`, `6NF`, `EKNF`, `ETNF`, and `DKNF`. We won't cover these here, but we will create a +dedicated set of tutorials for them in our guides and tutorials section. + +### Database Relationships #### One-to-One In a one-to-one relationship, each record in `table A` is related to at most one record in `table B`, and each record in `table B` is diff --git a/src/content/docs/rqb-v2.mdx b/src/content/docs/rqb-v2.mdx index 83603c26..9e664660 100644 --- a/src/content/docs/rqb-v2.mdx +++ b/src/content/docs/rqb-v2.mdx @@ -556,7 +556,23 @@ const response = db.query.users.findMany({ ### Relations Filters -// TODO +With Drizzle Relations, you can filter not only by the table you're querying but also by any table you include in the query. + +Example: Get all `users` whose ID>10 and who have at least one post with content starting with "M" +```ts +const usersWithPosts = await db.query.usersTable.findMany({ + where: { + id: { + gt: 10 + } + }, + posts: { + content: { + like: 'M%' + } + } +}) +``` ### Limit & Offset Drizzle ORM provides `limit` & `offset` API for queries and for the nested entities. From fe767f994f1bedbfe21330c0031070035525d547 Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Fri, 28 Feb 2025 18:01:20 +0200 Subject: [PATCH 03/10] Added schema --- src/content/docs/relations-v2.mdx | 8 ++--- src/content/docs/sql-schema-declaration.mdx | 33 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/content/docs/relations-v2.mdx b/src/content/docs/relations-v2.mdx index 9c71c082..8755f032 100644 --- a/src/content/docs/relations-v2.mdx +++ b/src/content/docs/relations-v2.mdx @@ -152,7 +152,7 @@ An example of a `one-to-one` relation between users and users, where a user can ```typescript copy {10-17} import { pgTable, serial, text, boolean } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; +import { defineRelations } from 'drizzle-orm'; export const users = pgTable('users', { id: integer().primaryKey(), @@ -174,7 +174,7 @@ Another example would be a user having a profile information stored in separate ```typescript copy {15-22} import { pgTable, serial, text, integer, jsonb } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; +import { defineRelations } from 'drizzle-orm'; export const users = pgTable('users', { id: integer().primaryKey(), @@ -207,7 +207,7 @@ Example of `one-to-many` relation between users and posts they've written: ```typescript copy {15-25} import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; +import { defineRelations } from 'drizzle-orm'; export const users = pgTable('users', { id: integer('id').primaryKey(), @@ -277,7 +277,7 @@ they have to be explicitly defined and store associations between related tables Example of `many-to-many` relation between users and groups we are using `through` to bypass junction table selection and directly select many `groups` for each `user`. ```typescript copy {27-39} -import { relations } from 'drizzle-orm'; +import { defineRelations } from 'drizzle-orm'; import { integer, pgTable, primaryKey, serial, text } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { diff --git a/src/content/docs/sql-schema-declaration.mdx b/src/content/docs/sql-schema-declaration.mdx index 6873624d..a211c2aa 100644 --- a/src/content/docs/sql-schema-declaration.mdx +++ b/src/content/docs/sql-schema-declaration.mdx @@ -17,6 +17,39 @@ and migrations (using Drizzle-Kit). If you are using Drizzle-Kit for the migration process, make sure to export all the models defined in your schema files so that Drizzle-Kit can import them and use them in the migration diff process. + +```ts +import { integer, pgTable, varchar } from "drizzle-orm/pg-core"; + +export const usersTable = pgTable("users", { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + name: varchar().notNull(), + age: integer().notNull(), + email: varchar().notNull().unique(), +}); +``` +```ts +import { pgTable } from "drizzle-orm/pg-core"; + +export const usersTable = pgTable("users", (t) => ({ + id: t.integer().primaryKey().generatedAlwaysAsIdentity(), + name: t.varchar().notNull(), + age: t.integer().notNull(), + email: t.varchar().notNull().unique(), +})); +``` +```ts +import * as p from "drizzle-orm/pg-core"; + +export const usersTable = p.pgTable("users", { + id: p.integer().primaryKey().generatedAlwaysAsIdentity(), + name: p.varchar().notNull(), + age: p.integer().notNull(), + email: p.varchar().notNull().unique(), +}); +``` + + ## Organize your schema files You can declare your SQL schema directly in TypeScript either in a single `schema.ts` file, or you can spread them around — whichever you prefer, all the freedom! From 561eece15224382db3159bd5bfa3210d77c80894 Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Tue, 11 Mar 2025 13:41:35 +0200 Subject: [PATCH 04/10] Add updates to all docs for rqbv2 --- src/content/docs/_meta.json | 2 + .../docs/relations-schema-declaration.mdx | 6 + src/content/docs/relations-v1-v2.mdx | 936 ++++++++++++++++++ src/content/docs/relations-v2.mdx | 363 ++++++- src/content/docs/rqb-v2.mdx | 111 ++- 5 files changed, 1352 insertions(+), 66 deletions(-) create mode 100644 src/content/docs/relations-v1-v2.mdx diff --git a/src/content/docs/_meta.json b/src/content/docs/_meta.json index 936231c1..8b3ecc58 100644 --- a/src/content/docs/_meta.json +++ b/src/content/docs/_meta.json @@ -6,6 +6,8 @@ ["tutorials", "Tutorials"], ["latest-releases", "Latest releases"], ["gotchas", "Gotchas"], + "---", + ["relations-v1-v2", "Relational Queries v1 to v2"], "Fundamentals", ["sql-schema-declaration", "Schema"], diff --git a/src/content/docs/relations-schema-declaration.mdx b/src/content/docs/relations-schema-declaration.mdx index ddd8b044..7074ae54 100644 --- a/src/content/docs/relations-schema-declaration.mdx +++ b/src/content/docs/relations-schema-declaration.mdx @@ -395,11 +395,17 @@ checks can introduce a small but potentially noticeable performance overhead. +- **Scenario**: Systems where data is distributed across multiple database nodes or clusters (common in sharded databases, cloud environments, and microservices). +- **Explanation**: Cross-node foreign keys can introduce significant complexity and performance overhead. Validating referential integrity requires communication between nodes, leading to increased latency. Distributed transactions needed to maintain consistency are also more complex and can be less performant than local transactions. In such architectures, application-level data integrity checks or eventual consistency models might be considered alternatives. +- **Scenario**: Integrating a relational database with older legacy systems or non-relational data stores (e.g., NoSQL, flat files, external APIs). +- **Explanation**: Legacy systems or non-relational data might not consistently adhere to the referential integrity rules enforced by foreign keys. Imposing foreign keys in such scenarios can lead to data import issues, data inconsistencies, and might necessitate complex data transformation or application-level integrity management instead. You might need to carefully evaluate the data quality and consistency of the external sources and potentially rely on application logic or ETL processes to ensure data integrity instead of strictly enforcing foreign keys at the database level. +You can also check out some great explanations from the PlanetScale team in their [article](https://planetscale.com/docs/learn/operating-without-foreign-key-constraints#why-does-planetscale-not-recommend-constraints) + ### Polymorphic Relations Polymorphic relationships are a more advanced concept that allows a single relationship to point to diff --git a/src/content/docs/relations-v1-v2.mdx b/src/content/docs/relations-v1-v2.mdx new file mode 100644 index 00000000..e408729e --- /dev/null +++ b/src/content/docs/relations-v1-v2.mdx @@ -0,0 +1,936 @@ +import Callout from '@mdx/Callout.astro'; +import Prerequisites from "@mdx/Prerequisites.astro"; +import Section from '@mdx/Section.astro'; +import Npx from "@mdx/Npx.astro"; +import CodeTabs from '@mdx/CodeTabs.astro'; +import CodeTab from '@mdx/CodeTab.astro'; + +# Migrating to Relational Queries version 2 + + +- **Drizzle Relations v1** - +- **Relational Queries v1** - +- **drizzle-kit pull** - +- **Relations Fundamentals** - + + + +Below is the table of contents. Click an item to jump to that section: + +- [What is working differently from v1](#what-was-changed-and-is-working-differently-from-v1) +- [New features in v2]() +- [How to migrate relations definition from v1 to v2]() +- [How to migrate queries from v1 to v2]() +- [Partial upgrade, or how to stay on v1 even after an upgrade?]() + + +### API changes +#### What is working differently from v1 + +{/* ##### Drizzle Relations schema definition */} + +One of the biggest updates were in **Relations Schema definition** + +The first difference is that you no longer need to specify `relations` for each table separately in different objects and +then pass them all to `drizzle()` along with your schema. In Relational Queries v2, you now have one dedicated place to +specify all the relations for all the tables you need. + +The `r` parameter in the callback provides comprehensive autocomplete +functionality - including all tables from your schema and functions such as `one`, `many`, and `through` - essentially +offering everything you need to specify your relations. + +```ts +// relations.ts +import * as schema from "./schema" +import { defineRelations } from "drizzle-orm" + +export const relations = defineRelations(schema, (r) => ({ + ... +})); +``` +```ts +// index.ts +import { relations } from "./relations" +import { drizzle } from "drizzle-orm/..." + +const db = drizzle(process.env.DATABASE_URL, { relations }) +``` + +##### What is different? + + +```ts copy {21-28} +import * as p from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +export const users = p.pgTable('users', { + id: p.integer().primaryKey(), + name: p.text(), + invitedBy: p.integer('invited_by'), +}); + +export const posts = p.pgTable('posts', { + id: p.integer().primaryKey(), + content: p.text(), + authorId: p.integer('author_id'), +}); +``` + + +**One place for all your relations** + + +```ts +import { relations } from "drizzle-orm/_relations"; +import { users, posts } from './schema'; + +export const usersRelation = relations(users, ({ one, many }) => ({ + invitee: one(users, { + fields: [users.invitedBy], + references: [users.id], + }), + posts: many(posts), +})); + +export const postsRelation = relations(posts, ({ one, many }) => ({ + author: one(users, { + fields: [posts.authorId], + references: [users.id], + }), +})); +``` + + + +```ts +import { defineRelations } from "drizzle-orm"; +import * as schema from "./schema"; + +export const relations = defineRelations(schema, (r) => ({ + users: { + invitee: r.one.users({ + from: r.users.invitedBy, + to: r.users.id, + }), + posts: r.many.posts(), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, +})); +``` + + +**Define `many` without `one`** + +In v1, if you wanted only the `many` side of a relationship, you had to specify the `one` side on the other end, +which made for a poor developer experience. + +In v2, you can simply use the `many` side without any additional steps + + +```ts +import { relations } from "drizzle-orm/_relations"; +import { users, posts } from './schema'; + +export const usersRelation = relations(users, ({ one, many }) => ({ + posts: many(posts), +})); + +export const postsRelation = relations(posts, ({ one, many }) => ({ + author: one(users, { + fields: [posts.authorId], + references: [users.id], + }), +})); +``` + + + +```ts +import { defineRelations } from "drizzle-orm"; +import * as schema from "./schema"; + +export const relations = defineRelations(schema, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, +})); +``` + + +**No modes in `drizzle()`** + +We found a way to use the same strategy for all MySQL dialects, so there's no need to specify them + + +```ts +import * as schema from './schema' + +const db = drizzle(process.env.DATABASE_URL, { mode: "planetscale", schema }); +// or +const db = drizzle(process.env.DATABASE_URL, { mode: "default", schema }); +``` + + + +```ts +import { relations } from './relations' + +const db = drizzle(process.env.DATABASE_URL, { relations }); +``` + + +**`from` and `to` upgrades** + +We've renamed `fields` to `from` and `references` to `to`, and we made both accept either a single value or an array + + +```ts +... +author: one(users, { + fields: [posts.authorId], + references: [users.id], +}), +... +``` + + + +```ts +... +author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, +}), +... +``` +```ts +... +author: r.one.users({ + from: [r.posts.authorId], + to: [r.users.id], +}), +... +``` + + +**`relationName` -> `alias`** + + +```ts +import { relations } from "drizzle-orm/_relations"; +import { users, posts } from './schema'; + +export const postsRelation = relations(posts, ({ one }) => ({ + author: one(users, { + fields: [posts.authorId], + references: [users.id], + relationName: "author_post", + }), +})); +``` + + + +```ts +import { defineRelations } from "drizzle-orm"; +import * as schema from "./schema"; + +export const relations = defineRelations(schema, (r) => ({ + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + alias: "author_post", + }), + }, +})); +``` + + +##### What is new? + +**`through` for many-to-many relations** + +Previously, you would need to query through a junction table and then map it out for every response + +You don't need to do it now! + + +```ts +import * as p from "drizzle-orm/pg-core"; + +export const users = p.pgTable("users", { + id: p.integer().primaryKey(), + name: p.text(), + verified: p.boolean().notNull(), +}); + +export const groups = p.pgTable("groups", { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const usersToGroups = p.pgTable( + "users_to_groups", + { + userId: p + .integer("user_id") + .notNull() + .references(() => users.id), + groupId: p + .integer("group_id") + .notNull() + .references(() => groups.id), + }, + (t) => [p.primaryKey({ columns: [t.userId, t.groupId] })] +); +``` + + + +```ts +export const usersRelations = relations(users, ({ many }) => ({ + usersToGroups: many(usersToGroups), +})); + +export const groupsRelations = relations(groups, ({ many }) => ({ + usersToGroups: many(usersToGroups), +})); + +export const usersToGroupsRelations = relations(usersToGroups, ({ one }) => ({ + group: one(groups, { + fields: [usersToGroups.groupId], + references: [groups.id], + }), + user: one(users, { + fields: [usersToGroups.userId], + references: [users.id], + }), +})); +``` +```ts +// Query example +const response = await db.query.users.findMany({ + with: { + usersToGroups: { + columns: {}, + with: { + group: true, + }, + }, + }, +}); +``` + + + +```ts +import * as schema from './schema'; +import { defineRelations } from 'drizzle-orm'; + +export const relations = defineRelations(schema, (r) => ({ + users: { + groups: r.many.groups({ + from: r.users.id.through(r.usersToGroups.userId), + to: r.groups.id.through(r.usersToGroups.groupId), + }), + }, + groups: { + participants: r.many.users(), + }, +})); +``` +```ts +// Query example +const response = await db.query.users.findMany({ + with: { + groups: true, + }, +}); +``` + + +**Predefined filters** + + +Was not supported in v1 + + + +```ts {10-12} +import * as schema from './schema'; +import { defineRelations } from 'drizzle-orm'; + +export const relations = defineRelations(schema, + (r) => ({ + groups: { + verifiedUsers: r.many.users({ + from: r.groups.id.through(r.usersToGroups.groupId), + to: r.users.id.through(r.usersToGroups.userId), + where: { + verified: true, + }, + }), + }, + }) +); +``` +```ts {4} +// Query example: get groups with all verified users +const response = await db.query.groups.findMany({ + with: { + verifiedUsers: true, + }, +}); +``` + + +##### `where` is now object + + +```ts +const response = db._query.users.findMany({ + where: (users, { eq }) => eq(users.id, 1), +}); +``` + + + +```ts +const response = db.query.users.findMany({ + where: { + id: 1, + }, +}); +``` + +For a complete API Reference please check our [Select Filters docs](/docs/rqb-v2#select-filters) + + + +```ts +// schema.ts +import { integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: integer("id").primaryKey(), + name: text("name"), + email: text("email").notNull(), + age: integer("age"), + createdAt: timestamp("created_at").defaultNow(), + lastLogin: timestamp("last_login"), + subscriptionEnd: timestamp("subscription_end"), + lastActivity: timestamp("last_activity"), + preferences: jsonb("preferences"), // JSON column for user settings/preferences + interests: text("interests").array(), // Array column for user interests +}); +``` +```ts +const response = db.query.users.findMany({ + where: { + AND: [ + { + OR: [ + { RAW: (table) => sql`LOWER(${table.name}) LIKE 'john%'` }, + { name: { ilike: "jane%" } }, + ], + }, + { + OR: [ + { RAW: (table) => sql`${table.preferences}->>'theme' = 'dark'` }, + { RAW: (table) => sql`${table.preferences}->>'theme' IS NULL` }, + ], + }, + { RAW: (table) => sql`${table.age} BETWEEN 25 AND 35` }, + ], + }, +}); +``` + + +##### `orderBy` is now object + + +```ts +const response = db._query.users.findMany({ + orderBy: (users, { asc }) => [asc(users.id)], +}); +``` + + + +```ts +const response = db.query.users.findMany({ + orderBy: { id: "asc" }, +}); +``` + + +##### Filtering by relations + + +Was not supported in v1 + + + +Example: Get all users whose ID>10 and who have at least one post with content starting with “M” + +```ts +const usersWithPosts = await db.query.usersTable.findMany({ + where: { + id: { + gt: 10 + } + }, + posts: { + content: { + like: 'M%' + } + } +}); +``` + + +##### Using offset on related objects + + +Was not supported in v1 + + + +```ts +await db.query.posts.findMany({ + limit: 5, + offset: 2, // correct ✅ + with: { + comments: { + offset: 3, // correct ✅ + limit: 3, + }, + }, +}); +``` + + +#### How to migrate relations schema definition from v1 to v2 + +##### **Option 1**: Using `drizzle-kit pull` + +In new version `drizzle-kit pull` supports pulling `relations.ts` file in a new syntax: + +##### Step 1 + + +drizzle-kit pull + + +#### Step 2 + +Transfer generated relations code from `drizzle/relations.ts` to the file you are using to specify your relations +```plaintext {4-5} + ├ 📂 drizzle + │ ├ 📂 meta + │ ├ 📜 migration.sql + │ ├ 📜 relations.ts ────────┐ + │ └ 📜 schema.ts | + ├ 📂 src │ + │ ├ 📂 db │ + │ │ ├ 📜 relations.ts <─────┘ + │ │ └ 📜 schema.ts + │ └ 📜 index.ts + └ … +``` + + +`drizzle/relations.ts` includes an import of all tables from `drizzle/schema.ts`, which looks like this: +```ts +import * as schema from './schema' +``` + +You may need to change this import to a file where ALL your schema tables are located. + +If there are multiple schema files, you can do the following: +```ts +import * as schema1 from './schema1' +import * as schema2 from './schema2' +... +``` + + +#### Step 3 + +Change drizzle database instance creation and provide `relations` object instead of `schema` + + +```ts +import * as schema from './schema' +import { drizzle } from 'drizzle-orm/...' + +const db = drizzle('', { schema }) +``` + + + +```ts +// should be imported from a file in Step 2 +import { relations } from './relations' +import { drizzle } from 'drizzle-orm/...' + +const db = drizzle('', { relations }) +``` + + + +If you had MySQL dialect, you can remove `mode` from `drizzle()` as long as it's not needed in version 2 + + + +##### Manual migration + +If you want to migrate manually, you can check our [Drizzle Relations section](/docs/relations-v2) for the complete API reference and examples of one-to-one, one-to-many, and many-to-many relations. + +#### How to migrate queries from v1 to v2 + +##### Migrate `where` statements + +You can check our [Select Filters docs](/docs/rqb-v2#select-filters) to see examples and a complete API reference. + +With the new syntax, you can use `AND`, `OR`, `NOT`, and `RAW`, plus all the filtering operators that +were previously available in Relations v1. + +**Examples** + + +```ts +const response = db.query.users.findMany({ + where: { + age: 15, + }, +}); +``` +```sql {3} +select "users"."id" as "id", "users"."name" as "name" +from "users" +where ("users"."age" = $1) +``` + + +```ts +const response = db.query.users.findMany({ + where: { + age: 15, + name: 'John' + }, +}); +``` +```sql {3} +select "users"."id" as "id", "users"."name" as "name" +from "users" +where ("users"."age" = $1 and "users"."name" = $2) +``` + + +```ts +const response = await db.query.users.findMany({ + where: { + OR: [ + { + id: { + gt: 10, + }, + }, + { + name: { + like: "John%", + }, + } + ], + }, +}); +``` +```sql {3} +select "users"."id" as "id", "users"."name" as "name" +from "users" +where ("users"."id" > $1 or "users"."name" like $2) +``` + + +```ts +const response = db.query.users.findMany({ + where: { + NOT: { + id: { + gt: 10, + }, + }, + name: { + like: "John%", + }, + }, +}); +``` +```sql {3} +select "users"."id" as "id", "users"."name" as "name" +from "users" +where (not "users"."id" > $1 and "users"."name" like $2) +``` + + +```ts +// schema.ts +import { integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: integer("id").primaryKey(), + name: text("name"), + email: text("email").notNull(), + age: integer("age"), + createdAt: timestamp("created_at").defaultNow(), + lastLogin: timestamp("last_login"), + subscriptionEnd: timestamp("subscription_end"), + lastActivity: timestamp("last_activity"), + preferences: jsonb("preferences"), // JSON column for user settings/preferences + interests: text("interests").array(), // Array column for user interests +}); +``` +```ts +const response = db.query.users.findMany({ + where: { + AND: [ + { + OR: [ + { RAW: (table) => sql`LOWER(${table.name}) LIKE 'john%'` }, + { name: { ilike: "jane%" } }, + ], + }, + { + OR: [ + { RAW: (table) => sql`${table.preferences}->>'theme' = 'dark'` }, + { RAW: (table) => sql`${table.preferences}->>'theme' IS NULL` }, + ], + }, + { RAW: (table) => sql`${table.age} BETWEEN 25 AND 35` }, + ], + }, +}); +``` +```sql +``` + + + +##### Migrate `orderBy` statements + +Order by was simplified to a single object, where you specify the column and the sort direction (`asc` or `desc`) + + +```ts +const response = db._query.users.findMany({ + orderBy: (users, { asc }) => [asc(users.id)], +}); +``` + + + +```ts +const response = db.query.users.findMany({ + orderBy: { id: "asc" }, +}); +``` + + +##### Migrate `many-to-many` queries + +Relational Queries v1 had a very complex way of managing many-to-many queries. +You had to use junction tables to query through them explicitly, and then map those tables out, like this: + +```ts +const response = await db.query.users.findMany({ + with: { + usersToGroups: { + columns: {}, + with: { + group: true, + }, + }, + }, +}); +``` + +After upgrading to Relational Queries v2, your many-to-many relation will look like this: + +```ts +import * as schema from './schema'; +import { defineRelations } from 'drizzle-orm'; + +export const relations = defineRelations(schema, (r) => ({ + users: { + groups: r.many.groups({ + from: r.users.id.through(r.usersToGroups.userId), + to: r.groups.id.through(r.usersToGroups.groupId), + }), + }, + groups: { + participants: r.many.users(), + }, +})); +``` + +And when you migrate your query, it will become this: + +```ts +// Query example +const response = await db.query.users.findMany({ + with: { + groups: true, + }, +}); +``` + +#### Partial upgrade or how to stay on RQB v1 even after an upgrade? + +We've made an upgrade in a way, that all previous queries and relations definitions are still available for you. In this case you can migrate your codebase query by query without a need for a huge refactoring + +##### Step 1: Change relations import + +To define relations using Relational Queries v1, you would need to import it from `drizzle-orm` + + +```ts +import { relations } from 'drizzle-orm'; +``` + + +In Relational Queries v2 we moved it to `drizzle-orm/_relations` to give you some time for a migration + + +```ts +import { relations } from "drizzle-orm/_relations"; +``` + + +##### Step 2: Replace your queries to `._query` + +To use Relational Queries v1 you had to write `db.query.` + + +```ts +await db.query.users.findMany(); +``` + + +In Relational Queries v2, we moved it to `db._query` so that `db.query` could be used for a new syntax, +while still giving you the option to use the old syntax via `db._query`. + +We had a long discussion about whether we should just deprecate `db.query` and replace it with +something like `db.query2` or `db.queryV2`. In the end, we decided that all new APIs should remain +as simple as `db.query`, and that requiring you to replace all of your queries with `db._query` if you +want to keep using the old syntax is preferable to forcing everyone in the future to use +`db.queryV2`, `db.queryV3`, `db.queryV4`, etc. + + +```ts +// Using RQBv1 +await db._query.users.findMany(); + +// Using RQBv2 +await db.query.users.findMany(); +``` + + +##### Step 3 + +Define new relations or pull them using [this guide](#how-to-migrate-relations-schema-definition-from-v1-to-v2), +then use them in your new queries or migrate your existing queries one by one. + + +### Internal changes + +1. Every `drizzle` database, `session`, `migrator` and `transaction` instance, gained 2 additional generic arguments for RQB v2 queries + + + +```ts +``` + + +```ts +``` + + + +2. Updated `DrizzleConfig` generic with `TRelations` argument and `relations: TRelations` field + + + +```ts +``` + + + +```ts +``` + + + +3. The following entities have been moved from `drizzle-orm` and `drizzle-orm/relations` to `drizzle-orm/_relations`. The original imports now +include new types used by Relational Queries v2, so make sure to update your imports if you intend to use the older types: + + +- `Relation` +- `Relations` +- `One` +- `Many` +- `TableRelationsKeysOnly` +- `ExtractTableRelationsFromSchema` +- `ExtractObjectValues` +- `ExtractRelationsFromTableExtraConfigSchema` +- `getOperators` +- `Operators` +- `getOrderByOperators` +- `OrderByOperators` +- `FindTableByDBName` +- `DBQueryConfig` +- `TableRelationalConfig` +- `TablesRelationalConfig` +- `RelationalSchemaConfig` +- `ExtractTablesWithRelations` +- `ReturnTypeOrValue` +- `BuildRelationResult` +- `NonUndefinedKeysOnly` +- `BuildQueryResult` +- `RelationConfig` +- `extractTablesRelationalConfig` +- `relations` +- `createOne` +- `createMany` +- `NormalizedRelation` +- `normalizeRelation` +- `createTableRelationsHelpers` +- `TableRelationsHelpers` +- `BuildRelationalQueryResult` +- `mapRelationalRow` + + +4. In the same manner, `${dialect}-core/query-builders/query` files were moved to `${dialect}-core/query-builders/_query` +with RQB v2's alternatives being put in their place + + + +```ts +``` + + + +```ts +``` + + diff --git a/src/content/docs/relations-v2.mdx b/src/content/docs/relations-v2.mdx index 8755f032..d739d356 100644 --- a/src/content/docs/relations-v2.mdx +++ b/src/content/docs/relations-v2.mdx @@ -11,6 +11,8 @@ import CodeTabs from '@mdx/CodeTabs.astro'; - **Relations Fundamentals** - get familiar with the concepts of foreign key constraints, soft relations, database normalization, etc - [read here](/docs/rqb-fundamentals) + - **Declare schema** - get familiar with how to define drizzle schemas - [read here](/docs/sql-schema-declaration) + - **Database connection** - get familiar with how to connect to database using drizzle - [read here](/docs/get-started-postgresql) The sole purpose of Drizzle relations is to let you query your relational data in the most simple and concise way: @@ -278,16 +280,16 @@ they have to be explicitly defined and store associations between related tables Example of `many-to-many` relation between users and groups we are using `through` to bypass junction table selection and directly select many `groups` for each `user`. ```typescript copy {27-39} import { defineRelations } from 'drizzle-orm'; -import { integer, pgTable, primaryKey, serial, text } from 'drizzle-orm/pg-core'; +import { integer, pgTable, primaryKey, text } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { - id: serial('id').primaryKey(), - name: text('name'), + id: integer().primaryKey(), + name: text(), }); export const groups = pgTable('groups', { - id: serial('id').primaryKey(), - name: text('name'), + id: integer().primaryKey(), + name: text(), }); export const usersToGroups = pgTable( @@ -457,6 +459,296 @@ export const relations = defineRelations(schema,(r) => ({ ## --- +### Performance + +When working with relations in Drizzle ORM, especially in applications with +significant data or complex queries, optimizing database performance is crucial. +Indexes play a vital role in speeding up data retrieval, particularly when querying +related data. This section outlines recommended indexing strategies for each type +of relationship defined using Drizzle ORM. + +##### One-to-one Relationships + +In a one-to-one relationship, like the "user invites user" example or the +"user has profile info" example, the key performance consideration is efficient joining +of the related tables. + + +For optimal performance in one-to-one relationships, you should create an index +on the foreign key column in the table that is being referenced +(the "target" table in the relation). + + + +When you query data with related one-to-one information, Drizzle performs a JOIN operation. An index on the foreign key column allows the database to quickly +locate the related row in the target table, significantly speeding up the join process. + + +**Example:** +```typescript +import * as p from 'drizzle-orm/pg-core'; +import { defineRelations } from 'drizzle-orm'; + +export const users = p.pgTable('users', { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const profileInfo = p.pgTable('profile_info', { + id: p.integer().primaryKey(), + userId: p.integer('user_id').references(() => users.id), + metadata: p.jsonb(), +}); + +export const relations = defineRelations({ users, profileInfo }, (r) => ({ + users: { + profileInfo: r.one.profileInfo({ + from: r.users.id, + to: r.profileInfo.userId, + }) + } +})); +``` + +To optimize queries fetching user data along with their profile information, +you should create an index on the `userId` column in the `profile_info` table. + +```typescript {13-15,21} +import * as p from 'drizzle-orm/pg-core'; +import { defineRelations } from 'drizzle-orm'; + +export const users = p.pgTable('users', { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const profileInfo = pgTable('profile_info', { + id: p.integer().primaryKey(), + userId: p.integer('user_id').references(() => users.id), + metadata: p.jsonb(), +}, (table) => [ + p.index('profile_info_user_id_idx').on(table.userId) +]); + +export const relations = defineRelations({ users, profileInfo }, (r) => ({ + users: { + profileInfo: r.one.profileInfo({ + from: r.users.id, + to: r.profileInfo.userId, + }) + } +})); +``` +```sql +CREATE INDEX idx_profile_info_user_id ON profile_info (user_id); +``` + +#### One-to-many Relationships + +Similar to one-to-one relationships, one-to-many relations benefit significantly +from indexing to optimize join operations. Consider the "users and posts" example where one user can have many posts. + + +For one-to-many relationships, create an index on the foreign key column in the table that represents the "many" side of the relationship (the table with the foreign key referencing the "one" side). + + + +When you fetch a user with their posts or posts with their authors, joins are performed. +Indexing the foreign key (`authorId` in `posts` table) allows the database to efficiently +retrieve all posts associated with a given user or quickly find the author of a post. + + +**Example:** +```typescript +import * as p from "drizzle-orm/pg-core"; +import { defineRelations } from 'drizzle-orm'; + +export const users = p.pgTable('users', { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const posts = p.pgTable('posts', { + id: p.integer().primaryKey(), + content: p.text(), + authorId: p.integer('author_id'), +}); + +export const relations = defineRelations({ users, posts }, (r) => ({ + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, + users: { + posts: r.many.posts(), + }, +})); +``` + +To optimize queries involving users and their posts, create an index on the `authorId` column in the `posts` table. + +```typescript {13-15} +import * as p from "drizzle-orm/pg-core"; +import { defineRelations } from 'drizzle-orm'; + +export const users = p.pgTable('users', { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const posts = p.pgTable('posts', { + id: p.integer().primaryKey(), + content: p.text(), + authorId: p.integer('author_id'), +}, (t) => [ + index('posts_author_id_idx').on(table.authorId) +]); + +export const relations = defineRelations({ users, posts }, (r) => ({ + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, + users: { + posts: r.many.posts(), + }, +})); +``` +```sql +CREATE INDEX idx_posts_author_id ON posts (author_id); +``` + +#### Many-to-many Relationships + +Many-to-many relationships, implemented using junction tables, require a slightly +more nuanced indexing strategy to ensure optimal query performance. +Consider the "users and groups" example with the `usersToGroups` junction table. + + +For many-to-many relationships, it is generally recommended to create the following +indexes on the junction table: +1. **Index on each foreign key column individually:** This optimizes queries that +filter or join based on a single side of the relationship +(e.g., finding all groups for a user OR all users in a group). +2. **Composite index on both foreign key columns together:** This is crucial for +efficiently resolving the many-to-many relationship itself. It speeds up queries that need to find the connections between both entities. + + + +When querying many-to-many relations, especially when using `through` in Drizzle ORM, the database needs to efficiently navigate the junction table. + +- Indexes on individual foreign key columns (`userId`, `groupId` in `usersToGroups`) help when you are querying from one side to find the other (e.g., "find groups for a user"). +- The composite index on `(userId, groupId)` in `usersToGroups` is particularly important for quickly finding all relationships defined in the junction table. This is used when Drizzle ORM resolves the `many-to-many` relation to fetch related entities. + + +**Example:** + +In the "users and groups" example, the `usersToGroups` junction table connects `users` and `groups`. + +```typescript +import { defineRelations } from 'drizzle-orm'; +import * as p from 'drizzle-orm/pg-core'; + +export const users = p.pgTable('users', { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const groups = p.pgTable('groups', { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const usersToGroups = p.pgTable( + 'users_to_groups', + { + userId: p.integer('user_id') + .notNull() + .references(() => users.id), + groupId: p.integer('group_id') + .notNull() + .references(() => groups.id), + }, + (t) => [p.primaryKey({ columns: [t.userId, t.groupId] })], +); + +export const relations = defineRelations({ users, groups, usersToGroups }, + (r) => ({ + users: { + groups: r.many.groups({ + from: r.users.id.through(r.usersToGroups.userId), + to: r.groups.id.through(r.usersToGroups.groupId), + }), + }, + groups: { + participants: r.many.users(), + }, + }) +); +``` + +To optimize queries for users and groups, create indexes on `usersToGroups` table as follows: + +```typescript {26-28} +import { defineRelations } from 'drizzle-orm'; +import * as p from 'drizzle-orm/pg-core'; + +export const users = p.pgTable('users', { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const groups = p.pgTable('groups', { + id: p.integer().primaryKey(), + name: p.text(), +}); + +export const usersToGroups = p.pgTable( + 'users_to_groups', + { + userId: p.integer('user_id') + .notNull() + .references(() => users.id), + groupId: p.integer('group_id') + .notNull() + .references(() => groups.id), + }, + (t) => [ + p.primaryKey({ columns: [t.userId, t.groupId] }), + p.index('users_to_groups_user_id_idx').on(table.userId), + p.index('users_to_groups_group_id_idx').on(table.groupId), + p.index('users_to_groups_composite_idx').on(table.userId, table.groupId), + ], +); + +export const relations = defineRelations({ users, groups, usersToGroups }, + (r) => ({ + users: { + groups: r.many.groups({ + from: r.users.id.through(r.usersToGroups.userId), + to: r.groups.id.through(r.usersToGroups.groupId), + }), + }, + groups: { + participants: r.many.users(), + }, + }) +); +``` +```sql +CREATE INDEX idx_users_to_groups_user_id ON users_to_groups (user_id); +CREATE INDEX idx_users_to_groups_group_id ON users_to_groups (group_id); +CREATE INDEX idx_users_to_groups_composite ON users_to_groups (userId, groupId); +``` + +By applying these indexing strategies, you can significantly improve the performance of your Drizzle ORM applications when working with relational data, especially as your data volume grows and your queries become more complex. Remember to choose the indexes that best suit your specific query patterns and application needs. + +## --- + ### Foreign keys You might've noticed that `relations` look similar to foreign keys — they even have a `references` property. So what's the difference? @@ -471,47 +763,52 @@ What this means is `relations` and foreign keys can be used together, but they a You can define `relations` without using foreign keys (and vice versa), which allows them to be used with databases that do not support foreign keys. The following two examples will work exactly the same in terms of querying the data using Drizzle relational queries. + ```ts {15} -export const users = pgTable('users', { - id: serial('id').primaryKey(), - name: text('name'), +export const users = p.pgTable("users", { + id: p.integer().primaryKey(), + name: p.text(), }); -export const usersRelations = relations(users, ({ one, many }) => ({ - profileInfo: one(users, { - fields: [profileInfo.userId], - references: [users.id], - }), -})); - -export const profileInfo = pgTable('profile_info', { - id: serial('id').primaryKey(), - userId: integer("user_id"), - metadata: jsonb("metadata"), +export const profileInfo = p.pgTable("profile_info", { + id: p.integer().primaryKey(), + userId: p.integer("user_id"), + metadata: p.jsonb(), }); + +export const relations = defineRelations({ users, profileInfo }, (r) => ({ + users: { + profileInfo: r.one.profileInfo({ + from: r.users.id, + to: r.profileInfo.userId, + }), + }, +})); ``` ```ts {15} -export const users = pgTable('users', { - id: serial('id').primaryKey(), - name: text('name'), +export const users = p.pgTable("users", { + id: p.integer().primaryKey(), + name: p.text(), }); -export const usersRelations = relations(users, ({ one, many }) => ({ - profileInfo: one(users, { - fields: [profileInfo.userId], - references: [users.id], - }), -})); - -export const profileInfo = pgTable('profile_info', { - id: serial('id').primaryKey(), - userId: integer("user_id").references(() => users.id), - metadata: jsonb("metadata"), +export const profileInfo = p.pgTable("profile_info", { + id: p.integer().primaryKey(), + userId: p.integer("user_id").references(() => users.id), + metadata: p.jsonb(), }); + +export const relations = defineRelations({ users, profileInfo }, (r) => ({ + users: { + profileInfo: r.one.profileInfo({ + from: r.users.id, + to: r.profileInfo.userId, + }), + }, +})); ``` diff --git a/src/content/docs/rqb-v2.mdx b/src/content/docs/rqb-v2.mdx index 9e664660..50027f0d 100644 --- a/src/content/docs/rqb-v2.mdx +++ b/src/content/docs/rqb-v2.mdx @@ -84,7 +84,6 @@ We've made sure you have both the best-in-class developer experience and perform ``` -## Querying Relational queries are an extension to Drizzle's original **[query builder](/docs/select)**. You need to provide all `tables` and `relations` from your schema file/files upon `drizzle()` initialization and then just use the `db.query` API. @@ -381,35 +380,32 @@ const users = await db.query.users.findMany({ } }); ``` -```ts copy -const users = await db.query.users.findMany({ - where: { - id: 1 - } -}); +```sql +select * from users where id = 1 ```
Find post with `id=1` and comments that were created before particular date: ```typescript copy await db.query.posts.findMany({ - where: { - id: 1, - }, - with: { - comments: { - where: { - createdAt: { lt: new Date() }, - }, + where: { + id: 1, + }, + with: { + comments: { + where: { + createdAt: { lt: new Date() }, }, }, - }); + }, +}); ``` **List of all filter operators** ```ts where: { OR: [], + AND: [], NOT: {}, RAW: (table) => sql`${table.id} = 1`, @@ -446,7 +442,7 @@ const response = db.query.users.findMany({ ```sql {3} select "users"."id" as "id", "users"."name" as "name" from "users" -where ("users"."age" = $1) +where ("users"."age" = 15) ``` @@ -461,7 +457,7 @@ const response = db.query.users.findMany({ ```sql {3} select "users"."id" as "id", "users"."name" as "name" from "users" -where ("users"."age" = $1 and "users"."name" = $2) +where ("users"."age" = 15 and "users"."name" = 'John') ``` @@ -486,7 +482,7 @@ const response = await db.query.users.findMany({ ```sql {3} select "users"."id" as "id", "users"."name" as "name" from "users" -where ("users"."id" > $1 or "users"."name" like $2) +where ("users"."id" > 10 or "users"."name" like 'John%') ``` @@ -507,7 +503,7 @@ const response = db.query.users.findMany({ ```sql {3} select "users"."id" as "id", "users"."name" as "name" from "users" -where (not "users"."id" > $1 and "users"."name" like $2) +where (not "users"."id" > 10 and "users"."name" like 'John%') ``` @@ -550,6 +546,15 @@ const response = db.query.users.findMany({ }); ``` ```sql +select "d0"."id" as "id", "d0"."name" as "name", +"d0"."email" as "email", "d0"."age" as "age", +"d0"."created_at" as "createdAt", "d0"."last_login" as "lastLogin", +"d0"."subscription_end" as "subscriptionEnd", "d0"."last_activity" as "lastActivity", +"d0"."preferences" as "preferences", "d0"."interests" as "interests" +from "users" as "d0" +where ((LOWER("d0"."name") LIKE 'john%' or "d0"."name" ilike 'jane%') +and ("d0"."preferences"->>'theme' = 'dark' or "d0"."preferences"->>'theme' IS NULL) +and "d0"."age" BETWEEN 25 AND 35) ``` @@ -558,7 +563,7 @@ const response = db.query.users.findMany({ With Drizzle Relations, you can filter not only by the table you're querying but also by any table you include in the query. -Example: Get all `users` whose ID>10 and who have at least one post with content starting with "M" +**Example:** Get all `users` whose ID>10 and who have at least one post with content starting with "M" ```ts const usersWithPosts = await db.query.usersTable.findMany({ where: { @@ -574,6 +579,18 @@ const usersWithPosts = await db.query.usersTable.findMany({ }) ``` +**Example:** Get all `users` with posts, only if user has at least 1 post +```ts +const response = db.query.users.findMany({ + with: { + posts: true, + }, + where: { + posts: true, + }, +}); +``` + ### Limit & Offset Drizzle ORM provides `limit` & `offset` API for queries and for the nested entities. @@ -663,14 +680,14 @@ import { sql } from 'drizzle-orm'; await db.query.users.findMany({ extras: { - loweredName: sql`lower(${users.name})`.as('lowered_name'), + loweredName: sql`lower(${users.name})`, }, }) ``` ```typescript copy {3} await db.query.users.findMany({ extras: { - loweredName: (users, { sql }) => sql`lower(${users.name})`.as('lowered_name'), + loweredName: (users, { sql }) => sql`lower(${users.name})`, }, }) ``` @@ -679,7 +696,7 @@ await db.query.users.findMany({ `lowerName` as a key will be included to all fields in returned object. - You have to explicitly specify `.as("")` + If you will specify `.as("")` for any extras field - drizzle will ignore it To retrieve all users with groups, but with the fullName field included (which is a concatenation of firstName and lastName), @@ -689,7 +706,7 @@ you can use the following query with the Drizzle relational query builder. ```typescript copy const res = await db.query.users.findMany({ extras: { - fullName: sql`concat(${users.name}, " ", ${users.name})`.as('full_name'), + fullName: sql`concat(${users.name}, " ", ${users.name})`, }, with: { usersToGroups: { @@ -726,12 +743,12 @@ To retrieve all posts with comments and add an additional field to calculate the ```typescript copy const res = await db.query.posts.findMany({ extras: (table, { sql }) => ({ - contentLength: (sql`length(${table.content})`).as('content_length'), + contentLength: (sql`length(${table.content})`), }), with: { comments: { extras: { - commentSize: sql`length(${comments.content})`.as('comment_size'), + commentSize: sql`length(${comments.content})`, }, }, }, @@ -757,6 +774,34 @@ const res: { ```
+### Include subqueries + +You can also use subqueries within Relational Queries to leverage the power of custom SQL syntax + +**Get users with posts and total posts count for each user** +```ts +import { posts } from './schema'; +import { eq } from 'drizzle-orm'; + +await db.query.users.findMany({ + with: { + posts: true + }, + extras: { + totalPostsCount: (table) => db.$count(posts, eq(posts.authorId, t.id)), + } +}); +``` +```sql +select "d0"."id" as "id", "d0"."name" as "name", "posts"."r" as "posts", +((select count(*) from "posts" where "posts"."author_id" = "d0"."id")) as "totalPostsCount" +from "users" as "d0" +left join lateral( + select coalesce(json_agg(row_to_json("t".*)), '[]') as "r" + from (select "d1"."id" as "id", "d1"."content" as "content", "d1"."author_id" as "authorId" from "posts" as "d1" where "d0"."id" = "d1"."author_id") as "t" +) as "posts" on true +``` + ### Prepared statements Prepared statements are designed to massively improve query performance — [see here.](/docs/perf-queries) @@ -791,7 +836,7 @@ const prepared = db.query.users.findMany({ where: { id: 1 }, }, }, -}).prepare("query_name"); +}).prepare(); const usersWithPosts = await prepared.execute({ id: 1 }); ``` @@ -807,7 +852,7 @@ const prepared = db.query.users.findMany({ where: { id: 1 }, }, }, -}).prepare("query_name"); +}).prepare(); const usersWithPosts = await prepared.execute({ id: 1 }); ``` @@ -842,7 +887,7 @@ const prepared = db.query.users.findMany({ limit: sql.placeholder("limit"), }, }, - }).prepare("query_name"); + }).prepare(); const usersWithPosts = await prepared.execute({ limit: 1 }); ``` @@ -857,7 +902,7 @@ const prepared = db.query.users.findMany({ limit: sql.placeholder("limit"), }, }, - }).prepare("query_name"); + }).prepare(); const usersWithPosts = await prepared.execute({ limit: 1 }); ``` @@ -950,7 +995,7 @@ const prepared = db.query.users.findMany({ limit: sql.placeholder("pLimit"), }, }, -}).prepare("query_name"); +}).prepare(); const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6 }); ``` @@ -971,7 +1016,7 @@ const prepared = db.query.users.findMany({ limit: sql.placeholder("pLimit"), }, }, -}).prepare("query_name"); +}).prepare(); const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6 }); ``` From 1de05c96d84aee7fdb9b065b2663f78469202332 Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Thu, 13 Mar 2025 12:54:00 +0200 Subject: [PATCH 05/10] few fixes --- src/content/docs/rqb-v2.mdx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/content/docs/rqb-v2.mdx b/src/content/docs/rqb-v2.mdx index 50027f0d..739f9bbd 100644 --- a/src/content/docs/rqb-v2.mdx +++ b/src/content/docs/rqb-v2.mdx @@ -411,6 +411,9 @@ where: { // `id` - is a column key id: { + OR: [], + AND: [], + NOT: {}, eq: 1, ne: 1, gt: 1, @@ -613,7 +616,7 @@ await db.query.posts.findMany({ ``` - `offset` is now can be used in with tables as well! + `offset` now can be used in with tables as well! ```typescript await db.query.posts.findMany({ @@ -645,6 +648,10 @@ Drizzle provides API for ordering in the relational query builder. You can use same ordering **[core API](/docs/select#order-by)** or use `order by` operator from the callback with no imports. + +When you use multiple `orderBy` statements in the same table, they will be included in the query in the same order in which you added them + +
```typescript copy await db.query.posts.findMany({ @@ -706,7 +713,7 @@ you can use the following query with the Drizzle relational query builder. ```typescript copy const res = await db.query.users.findMany({ extras: { - fullName: sql`concat(${users.name}, " ", ${users.name})`, + fullName: (users, { sql }) => sql`concat(${users.name}, " ", ${users.name})`, }, with: { usersToGroups: { @@ -742,13 +749,13 @@ To retrieve all posts with comments and add an additional field to calculate the
```typescript copy const res = await db.query.posts.findMany({ - extras: (table, { sql }) => ({ - contentLength: (sql`length(${table.content})`), - }), + extras: { + contentLength: (table, { sql }) => sql`length(${table.content})`, + }, with: { comments: { extras: { - commentSize: sql`length(${comments.content})`, + commentSize: (table, { sql }) => sql`length(${table.content})`, }, }, }, @@ -788,7 +795,7 @@ await db.query.users.findMany({ posts: true }, extras: { - totalPostsCount: (table) => db.$count(posts, eq(posts.authorId, t.id)), + totalPostsCount: (table) => db.$count(posts, eq(posts.authorId, table.id)), } }); ``` From 37600e5b56331df26bd9d73af71e06f37824563e Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Thu, 13 Mar 2025 18:19:11 +0200 Subject: [PATCH 06/10] Add examples for internal changes --- src/content/docs/relations-v1-v2.mdx | 92 +++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/src/content/docs/relations-v1-v2.mdx b/src/content/docs/relations-v1-v2.mdx index e408729e..d43b8763 100644 --- a/src/content/docs/relations-v1-v2.mdx +++ b/src/content/docs/relations-v1-v2.mdx @@ -857,12 +857,84 @@ then use them in your new queries or migrate your existing queries one by one. 1. Every `drizzle` database, `session`, `migrator` and `transaction` instance, gained 2 additional generic arguments for RQB v2 queries +**migrator** ```ts +export async function migrate< + TSchema extends Record +>( + db: NodePgDatabase, + config: MigrationConfig, +) { + ... +} ``` +```ts {3,5} +export async function migrate< + TSchema extends Record, + TRelations extends AnyRelations +>( + db: NodePgDatabase, + config: MigrationConfig, +) { + ... +} +``` + +**session** + ```ts +export class NodePgSession< + TFullSchema extends Record, + TSchema extends V1.TablesRelationalConfig, +> extends PgSession +``` + + +```ts {3,4,6} +export class NodePgSession< + TFullSchema extends Record, + TRelations extends AnyRelations, + TTablesConfig extends TablesRelationalConfig, + TSchema extends V1.TablesRelationalConfig, +> extends PgSession +``` + +**transaction** + +```ts +export class NodePgTransaction< + TFullSchema extends Record, + TSchema extends V1.TablesRelationalConfig, +> extends PgTransaction +``` + + +```ts {3,4,6} +export class NodePgTransaction< + TFullSchema extends Record, + TRelations extends AnyRelations, + TTablesConfig extends TablesRelationalConfig, + TSchema extends V1.TablesRelationalConfig, +> extends PgTransaction +``` + +**driver** + +```ts +export class NodePgDatabase< + TSchema extends Record = Record, +> extends PgDatabase +``` + + +```ts {3,4} +export class NodePgDatabase< + TSchema extends Record = Record, + TRelations extends AnyRelations = EmptyRelations, +> extends PgDatabase ``` @@ -872,11 +944,27 @@ then use them in your new queries or migrate your existing queries one by one. ```ts +export interface DrizzleConfig< + TSchema extends Record = Record +> { + logger?: boolean | Logger; + schema?: TSchema; + casing?: Casing; +} ``` -```ts +```ts {8} +export interface DrizzleConfig< + TSchema extends Record = Record, + TRelations extends AnyRelations = EmptyRelations, +> { + logger?: boolean | Logger; + schema?: TSchema; + casing?: Casing; + relations?: TRelations; +} ``` @@ -926,11 +1014,13 @@ with RQB v2's alternatives being put in their place ```ts +import { RelationalQueryBuilder, PgRelationalQuery } from './query-builders/query.ts'; ``` ```ts +import { _RelationalQueryBuilder, _PgRelationalQuery } from './query-builders/_query.ts'; ``` From c06f77c24ea932ecd21c92c6146d28cc7d5213f6 Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Thu, 13 Mar 2025 19:37:42 +0200 Subject: [PATCH 07/10] + --- src/content/docs/relations-v1-v2.mdx | 10 +++++----- src/content/docs/rqb-v2.mdx | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/content/docs/relations-v1-v2.mdx b/src/content/docs/relations-v1-v2.mdx index d43b8763..0ec6fc54 100644 --- a/src/content/docs/relations-v1-v2.mdx +++ b/src/content/docs/relations-v1-v2.mdx @@ -488,13 +488,13 @@ const usersWithPosts = await db.query.usersTable.findMany({ where: { id: { gt: 10 + }, + posts: { + content: { + like: 'M%' + } } }, - posts: { - content: { - like: 'M%' - } - } }); ``` diff --git a/src/content/docs/rqb-v2.mdx b/src/content/docs/rqb-v2.mdx index 739f9bbd..85f11988 100644 --- a/src/content/docs/rqb-v2.mdx +++ b/src/content/docs/rqb-v2.mdx @@ -409,8 +409,11 @@ where: { NOT: {}, RAW: (table) => sql`${table.id} = 1`, - // `id` - is a column key - id: { + // filter by relations + [relation]: {}, + + // filter by columns + [column]: { OR: [], AND: [], NOT: {}, @@ -572,14 +575,14 @@ const usersWithPosts = await db.query.usersTable.findMany({ where: { id: { gt: 10 + }, + posts: { + content: { + like: 'M%' + } } }, - posts: { - content: { - like: 'M%' - } - } -}) +}); ``` **Example:** Get all `users` with posts, only if user has at least 1 post From 7f0894ff1322dc8953b994eef4aa2f63ba3f45a9 Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Mon, 17 Mar 2025 16:23:09 +0200 Subject: [PATCH 08/10] Update docs --- src/content/docs/relations-schema-declaration.mdx | 7 ------- src/content/docs/relations-v1-v2.mdx | 9 +++++---- src/content/docs/relations-v2.mdx | 4 ++-- src/content/docs/rqb-v2.mdx | 13 +++++++++++++ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/content/docs/relations-schema-declaration.mdx b/src/content/docs/relations-schema-declaration.mdx index 7074ae54..ae118021 100644 --- a/src/content/docs/relations-schema-declaration.mdx +++ b/src/content/docs/relations-schema-declaration.mdx @@ -14,13 +14,6 @@ Think of "relations" as the connections and links between different pieces of da where people have relationships with each other, or objects are related to categories, databases use relations to model how different types of information are connected and work together. -{/* This page will guide you through the core ideas of database relations, explaining why they're so important and how they're structured. We'll cover: - -- **Database Normalization**: How to organize your data effectively. -- **Types of Relationships**: One-to-One, One-to-Many, and Many-to-Many. -- **Foreign Key Constraints**: How to enforce these relationships and keep your data accurate. -- **Polymorphic Relationships**: A glimpse into more complex connections. */} - ### Normalization Normalization is the process of organizing data in your database to reduce redundancy (duplication) and improve data integrity (accuracy and consistency). Think of it like tidying up a messy filing cabinet. Instead of having all sorts of papers diff --git a/src/content/docs/relations-v1-v2.mdx b/src/content/docs/relations-v1-v2.mdx index 0ec6fc54..e7015647 100644 --- a/src/content/docs/relations-v1-v2.mdx +++ b/src/content/docs/relations-v1-v2.mdx @@ -18,10 +18,11 @@ import CodeTab from '@mdx/CodeTab.astro'; Below is the table of contents. Click an item to jump to that section: - [What is working differently from v1](#what-was-changed-and-is-working-differently-from-v1) -- [New features in v2]() -- [How to migrate relations definition from v1 to v2]() -- [How to migrate queries from v1 to v2]() -- [Partial upgrade, or how to stay on v1 even after an upgrade?]() +- [New features in v2](2#what-is-new) +- [How to migrate relations definition from v1 to v2](#how-to-migrate-relations-schema-definition-from-v1-to-v2) +- [How to migrate queries from v1 to v2](#how-to-migrate-queries-from-v1-to-v2) +- [Partial upgrade, or how to stay on v1 even after an upgrade?](#partial-upgrade-or-how-to-stay-on-rqb-v1-even-after-an-upgrade) +- [Internal changes(imports, internal types, etc.)](#internal-changes) ### API changes diff --git a/src/content/docs/relations-v2.mdx b/src/content/docs/relations-v2.mdx index d739d356..8b7a355e 100644 --- a/src/content/docs/relations-v2.mdx +++ b/src/content/docs/relations-v2.mdx @@ -110,7 +110,7 @@ This should be used when you are certain that this specific entity will always e - `alias` is used to add a specific alias to relationships between tables. If you have multiple identical relationships between two tables, you should differentiate them using `alias` - `where` condition can be used for polymorphic relations. It fetches relations based on a `where` statement. -For example, in the case above, only `verified authors` will be retrieved. Learn more about polymorphic relations [here]. +For example, in the case above, only `verified authors` will be retrieved. Learn more about polymorphic relations [here](/docs/relations-schema-declaration#polymorphic-relations). ### `many()` @@ -143,7 +143,7 @@ This should be used when you are certain that this specific entity will always e - `alias` is used to add a specific alias to relationships between tables. If you have multiple identical relationships between two tables, you should differentiate them using `alias` - `where` condition can be used for polymorphic relations. It fetches relations based on a `where` statement. -For example, in the case above, only `approved posts` will be retrieved. Learn more about polymorphic relations [here]. +For example, in the case above, only `approved posts` will be retrieved. Learn more about polymorphic relations [here](/docs/relations-schema-declaration#polymorphic-relations). ## --- diff --git a/src/content/docs/rqb-v2.mdx b/src/content/docs/rqb-v2.mdx index 85f11988..3b9228f6 100644 --- a/src/content/docs/rqb-v2.mdx +++ b/src/content/docs/rqb-v2.mdx @@ -677,6 +677,19 @@ await db.query.posts.findMany({ }); ``` +You can also use custom `sql` in order by statement: + +```typescript copy +await db.query.posts.findMany({ + orderBy: (t) => sql`${t.id} asc`, + with: { + comments: { + orderBy: (t, { desc }) => desc(t.id), + }, + }, +}); +``` + ### Include custom fields Relational query API lets you add custom additional fields. It's useful when you need to retrieve data and apply additional functions to it. From 53b5b4cd44482b6462e87ebcb842eb1b449b9633 Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Wed, 9 Apr 2025 14:03:48 +0300 Subject: [PATCH 09/10] Add all new features to rqb docs --- src/content/docs/column-types/pg.mdx | 25 +++ src/content/docs/custom-types.mdx | 240 +++++++++++++++------------ src/content/docs/rqb-v2.mdx | 3 + 3 files changed, 162 insertions(+), 106 deletions(-) diff --git a/src/content/docs/column-types/pg.mdx b/src/content/docs/column-types/pg.mdx index 459fd793..21dc1d4d 100644 --- a/src/content/docs/column-types/pg.mdx +++ b/src/content/docs/column-types/pg.mdx @@ -239,6 +239,31 @@ CREATE TABLE IF NOT EXISTS "table" ( ## --- +### bytea + +PostgreSQL provides the standard SQL type bytea. + +For more info please refer to the official PostgreSQL **[docs.](https://www.postgresql.org/docs/current/datatype-binary.html)** + +
+```typescript +import { bytea, pgTable } from "drizzle-orm/pg-core"; + +export const table = pgTable('table', { + bytea: bytea() +}); + +``` + +```sql +CREATE TABLE IF NOT EXISTS "table" ( + "bytea" bytea, +); +``` +
+ +## --- + ### text `text` Variable-length(unlimited) character string. diff --git a/src/content/docs/custom-types.mdx b/src/content/docs/custom-types.mdx index 7a0df2c1..aa88dbfb 100644 --- a/src/content/docs/custom-types.mdx +++ b/src/content/docs/custom-types.mdx @@ -1,25 +1,39 @@ import Tab from '@mdx/Tab.astro'; import Tabs from '@mdx/Tabs.astro'; +import Callout from '@mdx/Callout.astro' # Common way of defining custom types ## Examples -The best way to see how `customType` definition is working is to check how existing data types in postgres and mysql could be defined using `customType` function from Drizzle ORM. +The best way to see how `customType` definition is working is to check how existing data types +could be defined using `customType` function from Drizzle ORM. - - + +Each dialect exposes `customType` function -**Serial** +```ts +import { customType } from 'drizzle-orm/pg-core'; +... +import { customType } from 'drizzle-orm/mysql-core'; +... +import { customType } from 'drizzle-orm/sqlite-core'; +... +import { customType } from 'drizzle-orm/gel-core'; +... +import { customType } from 'drizzle-orm/singlestore-core'; +``` + +**Integer** ```typescript copy import { customType } from 'drizzle-orm/pg-core'; -const customSerial = customType<{ data: number; notNull: true; default: true }>( +const customSerial = customType<{ data: number; }>( { dataType() { - return 'serial'; + return 'integer'; }, }, ); @@ -103,112 +117,15 @@ const usersTable = pgTable('users', { .default(sql`now()`), }); ``` - - - -**Serial** - -```typescript copy -import { customType } from 'drizzle-orm/mysql-core'; - -const customInt = customType<{ data: number; notNull: false; default: false }>( - { - dataType() { - return 'int'; - }, - }, -); -``` - -**Text** - -```typescript copy -import { customType } from 'drizzle-orm/mysql-core'; - -const customText = customType<{ data: string }>({ - dataType() { - return 'text'; - }, -}); -``` - -**Boolean** - -```typescript copy -import { customType } from 'drizzle-orm/mysql-core'; - -const customBoolean = customType<{ data: boolean }>({ - dataType() { - return 'boolean'; - }, - fromDriver(value) { - if (typeof value === 'boolean') { - return value; - } - return value === 1; - }, -}); -``` - -**Json** - -```typescript copy -import { customType } from 'drizzle-orm/mysql-core'; - -const customJson = (name: string) => - customType<{ data: TData; driverData: string }>({ - dataType() { - return 'json'; - }, - toDriver(value: TData): string { - return JSON.stringify(value); - }, - })(name); -``` - -**Timestamp** - -```typescript copy -import { customType } from 'drizzle-orm/mysql-core'; - -const customTimestamp = customType< - { data: Date; driverData: string; config: { fsp: number } } ->({ - dataType(config) { - const precision = typeof config.fsp !== 'undefined' - ? ` (${config.fsp})` - : ''; - return `timestamp${precision}`; - }, - fromDriver(value: string): Date { - return new Date(value); - }, -}); -``` - -Usage for all types will be same as defined functions in Drizzle ORM. For example: -```typescript copy -const usersTable = mysqlTable('userstest', { - id: customInt('id').primaryKey(), - name: customText('name').notNull(), - verified: customBoolean('verified').notNull().default(false), - jsonb: customJson('jsonb'), - createdAt: customTimestamp('created_at', { fsp: 2 }).notNull().default( - sql`now()`, - ), -}); -``` - - ## TS-doc for type definitions You can check ts-doc for `types` and `param` definition. ```typescript -export type CustomTypeValues = { +export interface CustomTypeValues = { /** * Required type for custom column, that will infer proper type model * @@ -220,11 +137,25 @@ export type CustomTypeValues = { */ data: unknown; + /** + * Type helper, that represents what type database driver is returning for specific database data type + * + * Needed only in case driver's output and input for type differ + * + * Defaults to {@link driverData} + */ + driverOutput?: unknown; + /** * Type helper, that represents what type database driver is accepting for specific database data type */ driverData?: unknown; + /** + * Type helper, that represents what type field returns after being aggregated to JSON for Relational Queries + */ + jsonData?: unknown; + /** * What config type should be used for {@link CustomTypeParams} `dataType` generation */ @@ -288,7 +219,7 @@ export interface CustomTypeParams { dataType: (config: T['config']) => string; /** - * Optional mapping function, between user input and driver + * Optional mapping function, between user input and what database driver will provide to the database * @example * For example, when using jsonb we need to map JS/TS object to string before writing to database * ``` @@ -300,7 +231,7 @@ export interface CustomTypeParams { toDriver?: (value: T['data']) => T['driverData']; /** - * Optional mapping function, that is responsible for data mapping from database to JS/TS code + * Optional mapping function, that is used for transforming data returned by driver to desired column's output format * @example * For example, when using timestamp we need to map string Date representation to JS Date * ``` @@ -308,7 +239,104 @@ export interface CustomTypeParams { * return new Date(value); * }, * ``` + * + * It'll cause the returned data to change from: + * ``` + * { + * customField: "2025-04-07T03:25:16.635Z"; + * } + * ``` + * to: + * ``` + * { + * customField: new Date("2025-04-07T03:25:16.635Z"); + * } + * ``` */ fromDriver?: (value: T['driverData']) => T['data']; + + /** + * Optional mapping function, that is used for transforming data returned by transofmed to JSON in database data to desired format + * + * Used by [relational queries](https://orm.drizzle.team/docs/rqb-v2) + * + * Defaults to {@link fromDriver} function + * @example + * For example, when querying bigint column via [RQB](https://orm.drizzle.team/docs/rqb-v2) or [JSON functions](https://orm.drizzle.team/docs/json-functions), the result field will be returned as it's string representation, as opposed to bigint from regular query + * To handle that, we need a separate function to handle such field's mapping: + * ``` + * fromJson(value: string): bigint { + * return BigInt(value); + * }, + * ``` + * + * It'll cause the returned data to change from: + * ``` + * { + * customField: "5044565289845416380"; + * } + * ``` + * to: + * ``` + * { + * customField: 5044565289845416380n; + * } + * ``` + */ + fromJson?: (value: T['jsonData']) => T['data']; + + /** + * Optional selection modifier function, that is used for modifying selection of column inside [JSON functions](https://orm.drizzle.team/docs/json-functions) + * + * Additional mapping that could be required for such scenarios can be handled using {@link fromJson} function + * + * Used by [relational queries](https://orm.drizzle.team/docs/rqb-v2) + * @example + * For example, when using bigint we need to cast field to text to preserve data integrity + * ``` + * forJsonSelect(identifier: SQL, sql: SQLGenerator, arrayDimensions?: number): SQL { + * return sql`${identifier}::text` + * }, + * ``` + * + * This will change query from: + * ``` + * SELECT + * row_to_json("t".*) + * FROM + * ( + * SELECT + * "table"."custom_bigint" AS "bigint" + * FROM + * "table" + * ) AS "t" + * ``` + * to: + * ``` + * SELECT + * row_to_json("t".*) + * FROM + * ( + * SELECT + * "table"."custom_bigint"::text AS "bigint" + * FROM + * "table" + * ) AS "t" + * ``` + * + * Returned by query object will change from: + * ``` + * { + * bigint: 5044565289845416000; // Partial data loss due to direct conversion to JSON format + * } + * ``` + * to: + * ``` + * { + * bigint: "5044565289845416380"; // Data is preserved due to conversion of field to text before JSON-ification + * } + * ``` + */ + forJsonSelect?: (identifier: SQL, sql: SQLGenerator, arrayDimensions?: number) => SQL; } ``` diff --git a/src/content/docs/rqb-v2.mdx b/src/content/docs/rqb-v2.mdx index 3b9228f6..27c69cee 100644 --- a/src/content/docs/rqb-v2.mdx +++ b/src/content/docs/rqb-v2.mdx @@ -431,6 +431,9 @@ where: { notIlike: "", isNull: true, isNotNull: true, + arrayOverlaps: [1, 2], + arrayContained: [1, 2], + arrayContains: [1, 2] }, }, ``` From 0a6a5bf6c4fe4fc5b5d205152d482fdfbaaa476a Mon Sep 17 00:00:00 2001 From: AndriiSherman Date: Wed, 9 Apr 2025 15:32:34 +0300 Subject: [PATCH 10/10] Update migration guide --- src/content/docs/relations-v1-v2.mdx | 119 +++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/src/content/docs/relations-v1-v2.mdx b/src/content/docs/relations-v1-v2.mdx index e7015647..98def386 100644 --- a/src/content/docs/relations-v1-v2.mdx +++ b/src/content/docs/relations-v1-v2.mdx @@ -166,6 +166,32 @@ export const relations = defineRelations(schema, (r) => ({ ``` +**New `optional` option** + +`optional: false` at the type level makes the `author` key in the `posts` object required. +This should be used when you are certain that this specific entity will always exist. + + +Was not supported in v1 + + + +```ts {9} +import { defineRelations } from "drizzle-orm"; +import * as schema from "./schema"; + +export const relations = defineRelations(schema, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + optional: false, + }), + }, +})); +``` + + **No modes in `drizzle()`** We found a way to use the same strategy for all MySQL dialects, so there's no need to specify them @@ -256,6 +282,99 @@ export const relations = defineRelations(schema, (r) => ({ ``` +**`custom types` new functions** + +There are a few new function were added to custom types, so you can control how data is mapped on Relational Queries v2: + + +Optional mapping function, that is used for transforming data returned by transformed to JSON in database data to desired format +For example, when querying bigint column via [RQB](https://orm.drizzle.team/docs/rqb-v2) or [JSON functions](https://orm.drizzle.team/docs/json-functions), the result field will be returned as it's string representation, as opposed to bigint from regular query +To handle that, we need a separate function to handle such field's mapping: +```ts +fromJson(value: string): bigint { + return BigInt(value); +}, +``` +It'll cause the returned data to change from: +```ts +{ + customField: "5044565289845416380"; +} +``` +to: +```ts +{ + customField: 5044565289845416380n; +} +``` + + +Optional selection modifier function, that is used for modifying selection of column inside [JSON functions](https://orm.drizzle.team/docs/json-functions) +Additional mapping that could be required for such scenarios can be handled using fromJson function +Used by [relational queries](https://orm.drizzle.team/docs/rqb-v2) + +For example, when using bigint we need to cast field to text to preserve data integrity +```ts +forJsonSelect(identifier: SQL, sql: SQLGenerator, arrayDimensions?: number): SQL { + return sql`${identifier}::text` +}, +``` +This will change query from: +```sql +SELECT + row_to_json("t".*) + FROM + ( + SELECT + "table"."custom_bigint" AS "bigint" + FROM + "table" + ) AS "t" +``` +to: +```sql +SELECT + row_to_json("t".*) + FROM + ( + SELECT + "table"."custom_bigint"::text AS "bigint" + FROM + "table" + ) AS "t" +``` +Returned by query object will change from: +```ts +{ + bigint: 5044565289845416000; // Partial data loss due to direct conversion to JSON format +} +``` +to: +```ts +{ + bigint: "5044565289845416380"; // Data is preserved due to conversion of field to text before JSON-ification +} +``` + + + +```ts +const customBytes = customType<{ + data: Buffer; + driverData: Buffer; + jsonData: string; + }>({ + dataType: () => 'bytea', + fromJson: (value) => { + return Buffer.from(value.slice(2, value.length), 'hex'); + }, + forJsonSelect: (identifier, sql, arrayDimensions) => + sql`${identifier}::text${sql.raw('[]'.repeat(arrayDimensions ?? 0))}`, + }); +``` + + + ##### What is new? **`through` for many-to-many relations**