diff --git a/drizzle-orm/src/column.ts b/drizzle-orm/src/column.ts index 3f3bcbf80..8265f06cb 100644 --- a/drizzle-orm/src/column.ts +++ b/drizzle-orm/src/column.ts @@ -105,6 +105,11 @@ export abstract class Column< mapToDriverValue(value: unknown): unknown { return value; } + + // ** @internal */ + isGenerated(): boolean { + return this.generated !== undefined; + } } export type UpdateColConfig< diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 34d5bf907..126903056 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -18,7 +18,7 @@ import { Param, type QueryWithTypings, SQL, sql, type SQLChunk, View } from '~/s import { Subquery, SubqueryConfig } from '~/subquery.ts'; import { getTableName, Table } from '~/table.ts'; import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; -import { DrizzleError, type Name, ViewBaseConfig, and, eq } from '../index.ts'; +import { and, DrizzleError, eq, type Name, ViewBaseConfig } from '../index.ts'; import { MySqlColumn } from './columns/common.ts'; import type { MySqlDeleteConfig } from './query-builders/delete.ts'; import type { MySqlInsertConfig } from './query-builders/insert.ts'; @@ -398,7 +398,7 @@ export class MySqlDialect { // const isSingleValue = values.length === 1; const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; const columns: Record = table[Table.Symbol.Columns]; - const colEntries: [string, MySqlColumn][] = Object.entries(columns); + const colEntries: [string, MySqlColumn][] = Object.entries(columns).filter(([_, col]) => !col.isGenerated()); const insertOrder = colEntries.map(([, column]) => sql.identifier(column.name)); diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 366436e29..9010b0016 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -24,6 +24,7 @@ import { type TableRelationalConfig, type TablesRelationalConfig, } from '~/relations.ts'; +import { and, eq, View } from '~/sql/index.ts'; import { type DriverValueEncoder, type Name, @@ -39,9 +40,8 @@ import { getTableName, Table } from '~/table.ts'; import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; import { ViewBaseConfig } from '~/view-common.ts'; import type { PgSession } from './session.ts'; -import type { PgMaterializedView } from './view.ts'; -import { View, and, eq } from '~/sql/index.ts'; import { PgViewBase } from './view-base.ts'; +import type { PgMaterializedView } from './view.ts'; export class PgDialect { static readonly [entityKind]: string = 'PgDialect'; @@ -426,7 +426,7 @@ export class PgDialect { const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; const columns: Record = table[Table.Symbol.Columns]; - const colEntries: [string, PgColumn][] = Object.entries(columns); + const colEntries: [string, PgColumn][] = Object.entries(columns).filter(([_, col]) => !col.isGenerated()); const insertOrder = colEntries.map(([, column]) => sql.identifier(column.name)); diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index d58ef419e..1e425ce2c 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -16,9 +16,9 @@ import { type TableRelationalConfig, type TablesRelationalConfig, } from '~/relations.ts'; +import type { Name } from '~/sql/index.ts'; +import { and, eq } from '~/sql/index.ts'; import { Param, type QueryWithTypings, SQL, sql, type SQLChunk } from '~/sql/sql.ts'; -import type { Name} from '~/sql/index.ts'; -import { and, eq } from '~/sql/index.ts' import { SQLiteColumn } from '~/sqlite-core/columns/index.ts'; import type { SQLiteDeleteConfig, SQLiteInsertConfig, SQLiteUpdateConfig } from '~/sqlite-core/query-builders/index.ts'; import { SQLiteTable } from '~/sqlite-core/table.ts'; @@ -365,7 +365,7 @@ export abstract class SQLiteDialect { const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; const columns: Record = table[Table.Symbol.Columns]; - const colEntries: [string, SQLiteColumn][] = Object.entries(columns); + const colEntries: [string, SQLiteColumn][] = Object.entries(columns).filter(([_, col]) => !col.isGenerated()); const insertOrder = colEntries.map(([, column]) => sql.identifier(column.name)); for (const [valueIndex, value] of values.entries()) { diff --git a/integration-tests/tests/libsql.test.ts b/integration-tests/tests/libsql.test.ts index b8e224e1b..11f5ca2a6 100644 --- a/integration-tests/tests/libsql.test.ts +++ b/integration-tests/tests/libsql.test.ts @@ -1348,7 +1348,7 @@ test.serial('insert null timestamp', async (t) => { t: integer('t', { mode: 'timestamp' }), }); - await db.run(sql`create table ${test} (t timestamp)`); + await db.run(sql`create table if not exists ${test} (t timestamp)`); await db.insert(test).values({ t: null }).run(); const res = await db.select().from(test).all(); @@ -2423,3 +2423,101 @@ test.serial('set operations (mixed all) as function with subquery', async (t) => ).orderBy(asc(sql`id`)); }); }); + +test.serial('select from a table with generated columns', async (t) => { + const { db } = t.context; + + const usersTable = sqliteTable('users', { + id: int('id').primaryKey({ autoIncrement: true }), + firstName: text('first_name'), + lastName: text('last_name'), + fullName: text('full_name').generatedAlwaysAs(sql`first_name || ' ' || last_name`, { mode: 'virtual' }), + fullName2: text('full_name2').generatedAlwaysAs(sql`first_name || ' ' || last_name`, { mode: 'stored' }), + upper: text('upper').generatedAlwaysAs(sql`upper(full_name)`, { mode: 'virtual' }), + }); + // const lkj = await db.get(sql`select * from ${usersTable}`); + // console.log(lkj); + + await db.run(sql`drop table if exists ${usersTable}`); + await db.run(sql` + create table ${usersTable} ( + id integer primary key autoincrement, + first_name text, + last_name text, + full_name text generated always as (first_name || ' ' || last_name) virtual, + full_name2 text generated always as (first_name || ' ' || last_name) stored, + upper text generated always as (upper(full_name)) virtual + ) + `); + + await db.insert(usersTable).values([ + { firstName: 'John', lastName: 'Doe' }, + { firstName: 'Jane', lastName: 'Doe' }, + ]); + + const result = await db.select().from(usersTable); + + Expect< + Equal<{ + id: number; + firstName: string | null; + lastName: string | null; + fullName: string; + fullName2: string; + upper: string; + }[], typeof result> + >; + + t.deepEqual(result, [ + { id: 1, firstName: 'John', lastName: 'Doe', fullName: 'John Doe', fullName2: 'John Doe', upper: 'JOHN DOE' }, + { id: 2, firstName: 'Jane', lastName: 'Doe', fullName: 'Jane Doe', fullName2: 'Jane Doe', upper: 'JANE DOE' }, + ]); +}); + +test.serial('select from a table with generated columns with null', async (t) => { + const { db } = t.context; + + const usersTable = sqliteTable('users', { + id: int('id').primaryKey({ autoIncrement: true }), + firstName: text('first_name'), + lastName: text('last_name'), + fullName: text('full_name').generatedAlwaysAs(sql`first_name || ' ' || last_name`, { mode: 'virtual' }).$type< + string | null + >(), + fullName2: text('full_name2').generatedAlwaysAs(sql`first_name || ' ' || last_name`, { mode: 'stored' }).$type< + string | null + >(), + upper: text('upper').generatedAlwaysAs(sql`upper(full_name)`, { mode: 'virtual' }).$type(), + }); + + await db.run(sql`drop table if exists ${usersTable}`); + await db.run(sql` + create table ${usersTable} ( + id integer primary key autoincrement, + first_name text, + last_name text, + full_name text generated always as (first_name || ' ' || last_name) virtual, + full_name2 text generated always as (first_name || ' ' || last_name) stored, + upper text generated always as (upper(full_name)) virtual + ) + `); + + await db.insert(usersTable).values({}); + + const result = await db.select().from(usersTable); + + Expect< + Equal<{ + id: number; + firstName: string | null; + lastName: string | null; + fullName: string | null; + fullName2: string | null; + upper: string | null; + }[], typeof result> + >; + + t.deepEqual(result, [ + { id: 1, firstName: null, lastName: null, fullName: null, fullName2: null, upper: null }, + ]); +}); diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index 3b545fcd8..23ca9c02c 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -2654,3 +2654,99 @@ test.serial('set operations (mixed all) as function with subquery', async (t) => ); }); }); + +test.serial('select from a table with generated columns', async (t) => { + const { db } = t.context; + + const usersTable = mysqlTable('users', { + id: serial('id'), + firstName: text('first_name'), + lastName: text('last_name'), + fullName: text('full_name').generatedAlwaysAs(sql`concat(first_name, ' ', last_name)`, { mode: 'virtual' }), + fullName2: text('full_name2').generatedAlwaysAs(sql`concat(first_name, ' ', last_name)`, { mode: 'stored' }), + upper: text('upper').generatedAlwaysAs(sql`upper(full_name)`, { mode: 'virtual' }), + }); + + await db.execute(sql`drop table if exists ${usersTable}`); + await db.execute(sql` + create table ${usersTable} ( + id serial, + first_name text, + last_name text, + full_name text generated always as (concat(first_name, ' ', last_name)) virtual, + full_name2 text generated always as (concat(first_name, ' ', last_name)) stored, + upper text generated always as (upper(full_name)) virtual + ) + `); + + await db.insert(usersTable).values([ + { firstName: 'John', lastName: 'Doe' }, + { firstName: 'Jane', lastName: 'Doe' }, + ]); + + const result = await db.select().from(usersTable); + + Expect< + Equal<{ + id: number; + firstName: string | null; + lastName: string | null; + fullName: string; + fullName2: string; + upper: string; + }[], typeof result> + >; + + t.deepEqual(result, [ + { id: 1, firstName: 'John', lastName: 'Doe', fullName: 'John Doe', fullName2: 'John Doe', upper: 'JOHN DOE' }, + { id: 2, firstName: 'Jane', lastName: 'Doe', fullName: 'Jane Doe', fullName2: 'Jane Doe', upper: 'JANE DOE' }, + ]); +}); + +test.serial('select from a table with generated columns with null', async (t) => { + const { db } = t.context; + + const usersTable = mysqlTable('users', { + id: serial('id'), + firstName: text('first_name'), + lastName: text('last_name'), + fullName: text('full_name').generatedAlwaysAs(sql`concat(first_name, ' ', last_name)`, { mode: 'virtual' }).$type< + string | null + >(), + fullName2: text('full_name2').generatedAlwaysAs(sql`concat(first_name, ' ', last_name)`, { mode: 'stored' }).$type< + string | null + >(), + upper: text('upper').generatedAlwaysAs(sql`upper(full_name)`, { mode: 'virtual' }).$type(), + }); + + await db.execute(sql`drop table if exists ${usersTable}`); + await db.execute(sql` + create table ${usersTable} ( + id serial, + first_name text, + last_name text, + full_name text generated always as (concat(first_name, ' ', last_name)) virtual, + full_name2 text generated always as (concat(first_name, ' ', last_name)) stored, + upper text generated always as (upper(full_name)) virtual + ) + `); + + await db.insert(usersTable).values({}); + + const result = await db.select().from(usersTable); + + Expect< + Equal<{ + id: number; + firstName: string | null; + lastName: string | null; + fullName: string | null; + fullName2: string | null; + upper: string | null; + }[], typeof result> + >; + + t.deepEqual(result, [ + { id: 1, firstName: null, lastName: null, fullName: null, fullName2: null, upper: null }, + ]); +}); diff --git a/integration-tests/tests/pg-proxy.test.ts b/integration-tests/tests/pg-proxy.test.ts index 02c48cffc..2c142d7d0 100644 --- a/integration-tests/tests/pg-proxy.test.ts +++ b/integration-tests/tests/pg-proxy.test.ts @@ -258,7 +258,7 @@ test.after.always(async (t) => { test.beforeEach(async (t) => { const ctx = t.context; - await ctx.db.execute(sql`drop schema public cascade`); + await ctx.db.execute(sql`drop schema if exists public cascade`); await ctx.db.execute(sql`create schema public`); await ctx.db.execute( sql` diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index 38fd1a8a3..b70e55ec6 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -42,6 +42,7 @@ import { macaddr, macaddr8, type PgColumn, + pgEnum, pgMaterializedView, pgTable, pgTableCreator, @@ -56,7 +57,6 @@ import { uniqueKeyName, uuid as pgUuid, varchar, - pgEnum, } from 'drizzle-orm/pg-core'; import getPort from 'get-port'; import pg from 'pg'; @@ -3151,3 +3151,99 @@ test.serial('set operations (mixed all) as function', async (t) => { ).orderBy(asc(sql`id`)); }); }); + +test.serial('select from a table with generated columns', async (t) => { + const { db } = t.context; + + const usersTable = pgTable('users', { + id: serial('id'), + firstName: text('first_name'), + lastName: text('last_name'), + fullName: text('full_name').generatedAlwaysAs(sql`first_name || ' ' || last_name`), + upper: text('upper').generatedAlwaysAs(sql`upper(full_name)`), + }); + + await db.execute(sql`drop table if exists ${usersTable}`); + await db.execute(sql` + create table ${usersTable} ( + id serial, + first_name text, + last_name text, + full_name text generated always as (CASE WHEN first_name IS NULL THEN last_name + WHEN last_name IS NULL THEN first_name + ELSE first_name || ' ' || last_name END) stored, + upper text generated always as (upper(CASE WHEN first_name IS NULL THEN last_name + WHEN last_name IS NULL THEN first_name + ELSE first_name || ' ' || last_name END)) stored + ) + `); + + await db.insert(usersTable).values([ + { firstName: 'John', lastName: 'Doe' }, + { firstName: 'Jane', lastName: 'Doe' }, + ]); + + const result = await db.select().from(usersTable); + + Expect< + Equal<{ + id: number; + firstName: string | null; + lastName: string | null; + fullName: string; + upper: string; + }[], typeof result> + >; + + t.deepEqual(result, [ + { id: 1, firstName: 'John', lastName: 'Doe', fullName: 'John Doe', upper: 'JOHN DOE' }, + { id: 2, firstName: 'Jane', lastName: 'Doe', fullName: 'Jane Doe', upper: 'JANE DOE' }, + ]); +}); + +test.serial('select from a table with generated columns with null', async (t) => { + const { db } = t.context; + + const usersTable = pgTable('users', { + id: serial('id'), + firstName: text('first_name'), + lastName: text('last_name'), + fullName: text('full_name').generatedAlwaysAs(sql`first_name || ' ' || last_name`).$type< + string | null + >(), + upper: text('upper').generatedAlwaysAs(sql`upper(full_name)`).$type(), + }); + + await db.execute(sql`drop table if exists ${usersTable}`); + await db.execute(sql` + create table ${usersTable} ( + id serial, + first_name text, + last_name text, + full_name text generated always as (CASE WHEN first_name IS NULL THEN last_name + WHEN last_name IS NULL THEN first_name + ELSE first_name || ' ' || last_name END) stored, + upper text generated always as (upper(CASE WHEN first_name IS NULL THEN last_name + WHEN last_name IS NULL THEN first_name + ELSE first_name || ' ' || last_name END)) stored + ) + `); + + await db.insert(usersTable).values({}); + + const result = await db.select().from(usersTable); + + Expect< + Equal<{ + id: number; + firstName: string | null; + lastName: string | null; + fullName: string | null; + upper: string | null; + }[], typeof result> + >; + + t.deepEqual(result, [ + { id: 1, firstName: null, lastName: null, fullName: null, upper: null }, + ]); +});