diff --git a/drizzle-kit/src/cli/commands/utils.ts b/drizzle-kit/src/cli/commands/utils.ts index d0abcee72e..6c18b6901e 100644 --- a/drizzle-kit/src/cli/commands/utils.ts +++ b/drizzle-kit/src/cli/commands/utils.ts @@ -444,6 +444,8 @@ export const preparePushConfig = async ( ), ); process.exit(1); + } else if (config.dialect === 'googlesql') { + throw new Error('Not implemented'); // TODO: SPANNER } assertUnreachable(config.dialect); @@ -643,6 +645,8 @@ export const preparePullConfig = async ( prefix: config.migrations?.prefix || 'index', entities: config.entities, }; + } else if (dialect === 'googlesql') { + throw new Error('Not implemented'); // TODO: SPANNER } assertUnreachable(dialect); @@ -754,6 +758,15 @@ export const prepareStudioConfig = async (options: Record) => { ), ); process.exit(1); + } else if (dialect === 'googlesql') { + throw new Error('Not implemented'); // TODO: SPANNER - not a priority + return { + dialect, + schema, + host, + port, + credentials: null as any, + }; } assertUnreachable(dialect); @@ -866,6 +879,17 @@ export const prepareMigrateConfig = async (configPath: string | undefined) => { process.exit(1); } + if (dialect === 'googlesql') { + throw new Error('Not implemented'); // TODO: SPANNER + return { + dialect, + out, + credentials: null as any, + schema, + table, + }; + } + assertUnreachable(dialect); }; diff --git a/drizzle-kit/src/cli/schema.ts b/drizzle-kit/src/cli/schema.ts index 2f8c410f47..0b15379bfa 100644 --- a/drizzle-kit/src/cli/schema.ts +++ b/drizzle-kit/src/cli/schema.ts @@ -106,6 +106,8 @@ export const generate = command({ ), ); process.exit(1); + } else if (dialect === 'googlesql') { + throw new Error('Not implemented'); // TODO: SPANNER } else { assertUnreachable(dialect); } @@ -208,6 +210,8 @@ export const migrate = command({ ), ); process.exit(1); + } else if (dialect === 'googlesql') { + throw new Error('Not implemented'); // TODO: SPANNER } else { assertUnreachable(dialect); } @@ -393,6 +397,13 @@ export const push = command({ ), ); process.exit(1); + } else if (dialect === 'googlesql') { + console.log( + error( + `You can't use 'push' command with GoogleSql dialect`, + ), + ); + process.exit(1); } else { assertUnreachable(dialect); } @@ -464,6 +475,10 @@ export const up = command({ ); process.exit(1); } + + if (dialect === 'googlesql') { + throw new Error('Not implemented'); // TODO: SPANNER + } }, }); @@ -620,6 +635,13 @@ export const pull = command({ prefix, entities, ); + } else if (dialect === 'googlesql') { + console.log( + error( + `You can't use 'pull' command with GoogleSql dialect`, + ), + ); + process.exit(1); } else { assertUnreachable(dialect); } @@ -745,6 +767,13 @@ export const studio = command({ ), ); process.exit(1); + } else if (dialect === 'googlesql') { + console.log( + error( + `You can't use 'studio' command with GoogleSql dialect`, + ), + ); + process.exit(1); } else { assertUnreachable(dialect); } @@ -847,6 +876,8 @@ export const exportRaw = command({ ), ); process.exit(1); + } else if (dialect === 'googlesql') { + throw new Error('Not implemented'); // TODO: SPANNER - probably not a priority } else { assertUnreachable(dialect); } diff --git a/drizzle-kit/src/cli/validations/googlesql.ts b/drizzle-kit/src/cli/validations/googlesql.ts new file mode 100644 index 0000000000..a34c22264d --- /dev/null +++ b/drizzle-kit/src/cli/validations/googlesql.ts @@ -0,0 +1,65 @@ +import { boolean, coerce, object, string, TypeOf, union } from 'zod'; +import { error } from '../views'; +import { wrapParam } from './common'; +import { outputs } from './outputs'; + +// TODO: SPANNER - add proper credentials config + +export const googlesqlCredentials = union([ + object({ + host: string().min(1), + port: coerce.number().min(1).optional(), + user: string().min(1).optional(), + // password: string().min(1).optional(), + // database: string().min(1), + // ssl: union([ + // string(), + // object({ + // pfx: string().optional(), + // key: string().optional(), + // passphrase: string().optional(), + // cert: string().optional(), + // ca: union([string(), string().array()]).optional(), + // crl: union([string(), string().array()]).optional(), + // ciphers: string().optional(), + // rejectUnauthorized: boolean().optional(), + // }), + // ]).optional(), + }), + object({ + url: string().min(1), + }), +]); + +export type GoogleSqlCredentials = TypeOf; + +// TODO: SPANNER - add proper connection issues +export const printCliConnectionIssues = (options: any) => { + // const { uri, host, database } = options || {}; + + // if (!uri && (!host || !database)) { + // console.log(outputs.googlesql.connection.required()); + // } +}; + +// TODO: SPANNER - add proper connection issues +export const printConfigConnectionIssues = ( + options: Record, +) => { + // if ('url' in options) { + // let text = `Please provide required params for MySQL driver:\n`; + // console.log(error(text)); + // console.log(wrapParam('url', options.url, false, 'url')); + // process.exit(1); + // } + + // let text = `Please provide required params for MySQL driver:\n`; + // console.log(error(text)); + // console.log(wrapParam('host', options.host)); + // console.log(wrapParam('port', options.port, true)); + // console.log(wrapParam('user', options.user, true)); + // console.log(wrapParam('password', options.password, true, 'secret')); + // console.log(wrapParam('database', options.database)); + // console.log(wrapParam('ssl', options.ssl, true)); + // process.exit(1); +}; diff --git a/drizzle-kit/src/cli/validations/outputs.ts b/drizzle-kit/src/cli/validations/outputs.ts index 6e9d520dd6..d93197643a 100644 --- a/drizzle-kit/src/cli/validations/outputs.ts +++ b/drizzle-kit/src/cli/validations/outputs.ts @@ -26,7 +26,7 @@ export const outputs = { ), noDialect: () => withStyle.error( - `Please specify 'dialect' param in config, either of 'postgresql', 'mysql', 'sqlite', turso or singlestore`, + `Please specify 'dialect' param in config, either of 'postgresql', 'mysql', 'sqlite', 'googlesql', turso or singlestore`, ), }, common: { @@ -88,4 +88,13 @@ export const outputs = { ), }, }, + googlesql: { + connection: { + driver: () => withStyle.error(`Only "spanner" is available options for "--driver"`), + required: () => + withStyle.error( + `Either "url" or "host", "database" are required for database connection`, // TODO: SPANNER - write proper error message + ), + }, + }, }; diff --git a/drizzle-kit/src/index.ts b/drizzle-kit/src/index.ts index e3d3d33134..2b597c8059 100644 --- a/drizzle-kit/src/index.ts +++ b/drizzle-kit/src/index.ts @@ -252,6 +252,7 @@ export type Config = }) ); } + // TODO: SPANNER - add googlesql/spanner config ); /** @@ -261,7 +262,7 @@ export type Config = * **Config** usage: * * `dialect` - mandatory and is responsible for explicitly providing a databse dialect you are using for all the commands - * *Possible values*: `postgresql`, `mysql`, `sqlite`, `singlestore`, `gel` + * *Possible values*: `postgresql`, `mysql`, `sqlite`, `singlestore`, `gel`, `googlesql` * * See https://orm.drizzle.team/kit-docs/config-reference#dialect * diff --git a/drizzle-kit/src/migrationPreparator.ts b/drizzle-kit/src/migrationPreparator.ts index 4e67e8174b..ada4489cde 100644 --- a/drizzle-kit/src/migrationPreparator.ts +++ b/drizzle-kit/src/migrationPreparator.ts @@ -168,6 +168,15 @@ export const prepareSqliteMigrationSnapshot = async ( return { prev: prevSnapshot, cur: result, custom }; }; +// TODO: SPANNER - implement +export const prepareGoogleSqlMigrationSnapshot = async ( + migrationFolders: string[], + schemaPath: string | string[], + casing: CasingType | undefined, +): Promise<{ prev: MySqlSchema; cur: MySqlSchema; custom: MySqlSchema }> => { + throw new Error('Not implemented'); +}; + export const fillPgSnapshot = ({ serialized, id, diff --git a/drizzle-kit/src/schemaValidator.ts b/drizzle-kit/src/schemaValidator.ts index ce4b2e59c4..0c6aa30de9 100644 --- a/drizzle-kit/src/schemaValidator.ts +++ b/drizzle-kit/src/schemaValidator.ts @@ -4,7 +4,7 @@ import { pgSchema, pgSchemaSquashed } from './serializer/pgSchema'; import { singlestoreSchema, singlestoreSchemaSquashed } from './serializer/singlestoreSchema'; import { sqliteSchema, SQLiteSchemaSquashed } from './serializer/sqliteSchema'; -export const dialects = ['postgresql', 'mysql', 'sqlite', 'turso', 'singlestore', 'gel'] as const; +export const dialects = ['postgresql', 'mysql', 'sqlite', 'turso', 'singlestore', 'gel', 'googlesql'] as const; export const dialect = enumType(dialects); export type Dialect = (typeof dialects)[number]; @@ -17,6 +17,7 @@ const commonSquashedSchema = union([ singlestoreSchemaSquashed, ]); +// TODO: SPANNER SCHEMA? const commonSchema = union([pgSchema, mysqlSchema, sqliteSchema, singlestoreSchema]); export type CommonSquashedSchema = TypeOf; diff --git a/drizzle-kit/src/sqlgenerator.ts b/drizzle-kit/src/sqlgenerator.ts index 4843c6c0c1..491c1ba517 100644 --- a/drizzle-kit/src/sqlgenerator.ts +++ b/drizzle-kit/src/sqlgenerator.ts @@ -3874,6 +3874,8 @@ class SingleStoreRecreateTableConvertor extends Convertor { } } +// TODO: SPANNER - add googlesql/spanner classes + const convertors: Convertor[] = []; convertors.push(new PgCreateTableConvertor()); convertors.push(new MySqlCreateTableConvertor()); diff --git a/drizzle-kit/src/utils.ts b/drizzle-kit/src/utils.ts index 1ee5f9d9a4..a1230e2198 100644 --- a/drizzle-kit/src/utils.ts +++ b/drizzle-kit/src/utils.ts @@ -128,6 +128,8 @@ const validatorForDialect = (dialect: Dialect) => { return { validator: backwardCompatibleSingleStoreSchema, version: 1 }; case 'gel': return { validator: backwardCompatibleGelSchema, version: 1 }; + case 'googlesql': + throw new Error('Not implemented'); // TODO: SPANNER } }; diff --git a/drizzle-kit/tests/cli-generate.test.ts b/drizzle-kit/tests/cli-generate.test.ts index a4adf979f2..917eadf5b3 100644 --- a/drizzle-kit/tests/cli-generate.test.ts +++ b/drizzle-kit/tests/cli-generate.test.ts @@ -2,6 +2,8 @@ import { test as brotest } from '@drizzle-team/brocli'; import { assert, expect, test } from 'vitest'; import { generate } from '../src/cli/schema'; +// TODO: SPANNER - add tests + // good: // #1 drizzle-kit generate --dialect=postgresql --schema=schema.ts // #2 drizzle-kit generate --dialect=postgresql --schema=schema.ts --out=out diff --git a/drizzle-kit/tests/cli-migrate.test.ts b/drizzle-kit/tests/cli-migrate.test.ts index 1425691f0b..644d63712e 100644 --- a/drizzle-kit/tests/cli-migrate.test.ts +++ b/drizzle-kit/tests/cli-migrate.test.ts @@ -2,6 +2,8 @@ import { test as brotest } from '@drizzle-team/brocli'; import { assert, expect, test } from 'vitest'; import { migrate } from '../src/cli/schema'; +// TODO: SPANNER - add tests + // good: // #1 drizzle-kit generate // #2 drizzle-kit generate --config=turso.config.ts diff --git a/drizzle-orm/src/column-builder.ts b/drizzle-orm/src/column-builder.ts index 1cc4c5ae1e..8a17facc63 100644 --- a/drizzle-orm/src/column-builder.ts +++ b/drizzle-orm/src/column-builder.ts @@ -1,6 +1,7 @@ import { entityKind } from '~/entity.ts'; import type { Column } from './column.ts'; import type { GelColumn, GelExtraConfigColumn } from './gel-core/index.ts'; +import type { GoogleSqlColumn } from './googlesql/index.ts'; import type { MySqlColumn } from './mysql-core/index.ts'; import type { ExtraConfigColumn, PgColumn, PgSequenceOptions } from './pg-core/index.ts'; import type { SingleStoreColumn } from './singlestore-core/index.ts'; @@ -25,7 +26,7 @@ export type ColumnDataType = | 'localDate' | 'localDateTime'; -export type Dialect = 'pg' | 'mysql' | 'sqlite' | 'singlestore' | 'common' | 'gel'; +export type Dialect = 'pg' | 'mysql' | 'sqlite' | 'singlestore' | 'common' | 'gel' | 'googlesql'; export type GeneratedStorageMode = 'virtual' | 'stored'; @@ -325,6 +326,20 @@ export type BuildColumn< {}, Simplify | 'brand' | 'dialect'>> > + : TDialect extends 'googlesql' ? GoogleSqlColumn< + MakeColumnConfig, + {}, + Simplify< + Omit< + TBuilder['_'], + | keyof MakeColumnConfig + | 'brand' + | 'dialect' + | 'primaryKeyHasDefault' + | 'googlesqlColumnBuilderBrand' + > + > + > : TDialect extends 'mysql' ? MySqlColumn< MakeColumnConfig, {}, @@ -410,6 +425,7 @@ export type BuildExtraConfigColumns< export type ChangeColumnTableName = TDialect extends 'pg' ? PgColumn> : TDialect extends 'mysql' ? MySqlColumn> + : TDialect extends 'googlesql' ? GoogleSqlColumn> : TDialect extends 'singlestore' ? SingleStoreColumn> : TDialect extends 'sqlite' ? SQLiteColumn> : TDialect extends 'gel' ? GelColumn> diff --git a/drizzle-orm/src/googlesql/alias.ts b/drizzle-orm/src/googlesql/alias.ts new file mode 100644 index 0000000000..6807e808f5 --- /dev/null +++ b/drizzle-orm/src/googlesql/alias.ts @@ -0,0 +1,11 @@ +import { TableAliasProxyHandler } from '~/alias.ts'; +import type { BuildAliasTable } from './query-builders/select.types.ts'; +import type { GoogleSqlTable } from './table.ts'; +import type { GoogleSqlViewBase } from './view-base.ts'; + +export function alias( + table: TTable, + alias: TAlias, +): BuildAliasTable { + return new Proxy(table, new TableAliasProxyHandler(alias, false)) as any; +} diff --git a/drizzle-orm/src/googlesql/checks.ts b/drizzle-orm/src/googlesql/checks.ts new file mode 100644 index 0000000000..d96cb9564b --- /dev/null +++ b/drizzle-orm/src/googlesql/checks.ts @@ -0,0 +1,32 @@ +import { entityKind } from '~/entity.ts'; +import type { SQL } from '~/sql/sql.ts'; +import type { GoogleSqlTable } from './table.ts'; + +export class CheckBuilder { + static readonly [entityKind]: string = 'GoogleSqlCheckBuilder'; + + protected brand!: 'GoogleSqlConstraintBuilder'; + + constructor(public name: string, public value: SQL) {} + + /** @internal */ + build(table: GoogleSqlTable): Check { + return new Check(table, this); + } +} + +export class Check { + static readonly [entityKind]: string = 'GoogleSqlCheck'; + + readonly name: string; + readonly value: SQL; + + constructor(public table: GoogleSqlTable, builder: CheckBuilder) { + this.name = builder.name; + this.value = builder.value; + } +} + +export function check(name: string, value: SQL): CheckBuilder { + return new CheckBuilder(name, value); +} diff --git a/drizzle-orm/src/googlesql/columns/all.ts b/drizzle-orm/src/googlesql/columns/all.ts new file mode 100644 index 0000000000..cf14d2e97c --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/all.ts @@ -0,0 +1,58 @@ +import { bigint } from './bigint.ts'; +import { binary } from './binary.ts'; +import { boolean } from './boolean.ts'; +import { char } from './char.ts'; +import { customType } from './custom.ts'; +import { date } from './date.ts'; +import { datetime } from './datetime.ts'; +import { decimal } from './decimal.ts'; +import { double } from './double.ts'; +import { googlesqlEnum } from './enum.ts'; +import { float } from './float.ts'; +import { int } from './int.ts'; +import { json } from './json.ts'; +import { mediumint } from './mediumint.ts'; +import { real } from './real.ts'; +import { serial } from './serial.ts'; +import { smallint } from './smallint.ts'; +import { longtext, mediumtext, text, tinytext } from './text.ts'; +import { time } from './time.ts'; +import { timestamp } from './timestamp.ts'; +import { tinyint } from './tinyint.ts'; +import { varbinary } from './varbinary.ts'; +import { varchar } from './varchar.ts'; +import { year } from './year.ts'; + +export function getGoogleSqlColumnBuilders() { + return { + bigint, + binary, + boolean, + char, + customType, + date, + datetime, + decimal, + double, + googlesqlEnum, + float, + int, + json, + mediumint, + real, + serial, + smallint, + text, + time, + timestamp, + tinyint, + varbinary, + varchar, + year, + longtext, + mediumtext, + tinytext, + }; +} + +export type GoogleSqlColumnBuilders = ReturnType; diff --git a/drizzle-orm/src/googlesql/columns/bigint.ts b/drizzle-orm/src/googlesql/columns/bigint.ts new file mode 100644 index 0000000000..8e21469ccb --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/bigint.ts @@ -0,0 +1,118 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; + +export type GoogleSqlBigInt53BuilderInitial = GoogleSqlBigInt53Builder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlBigInt53'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class GoogleSqlBigInt53Builder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlBigInt53Builder'; + + constructor(name: T['name'], unsigned: boolean = false) { + super(name, 'number', 'GoogleSqlBigInt53'); + this.config.unsigned = unsigned; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlBigInt53> { + return new GoogleSqlBigInt53>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlBigInt53> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlBigInt53'; + + getSQLType(): string { + return `bigint${this.config.unsigned ? ' unsigned' : ''}`; + } + + override mapFromDriverValue(value: number | string): number { + if (typeof value === 'number') { + return value; + } + return Number(value); + } +} + +export type GoogleSqlBigInt64BuilderInitial = GoogleSqlBigInt64Builder<{ + name: TName; + dataType: 'bigint'; + columnType: 'GoogleSqlBigInt64'; + data: bigint; + driverParam: string; + enumValues: undefined; +}>; + +export class GoogleSqlBigInt64Builder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlBigInt64Builder'; + + constructor(name: T['name'], unsigned: boolean = false) { + super(name, 'bigint', 'GoogleSqlBigInt64'); + this.config.unsigned = unsigned; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlBigInt64> { + return new GoogleSqlBigInt64>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlBigInt64> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlBigInt64'; + + getSQLType(): string { + return `bigint${this.config.unsigned ? ' unsigned' : ''}`; + } + + // eslint-disable-next-line unicorn/prefer-native-coercion-functions + override mapFromDriverValue(value: string): bigint { + return BigInt(value); + } +} + +export interface GoogleSqlBigIntConfig { + mode: T; + unsigned?: boolean; +} + +export function bigint( + config: GoogleSqlBigIntConfig, +): TMode extends 'number' ? GoogleSqlBigInt53BuilderInitial<''> : GoogleSqlBigInt64BuilderInitial<''>; +export function bigint( + name: TName, + config: GoogleSqlBigIntConfig, +): TMode extends 'number' ? GoogleSqlBigInt53BuilderInitial : GoogleSqlBigInt64BuilderInitial; +export function bigint(a?: string | GoogleSqlBigIntConfig, b?: GoogleSqlBigIntConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config.mode === 'number') { + return new GoogleSqlBigInt53Builder(name, config.unsigned); + } + return new GoogleSqlBigInt64Builder(name, config.unsigned); +} diff --git a/drizzle-orm/src/googlesql/columns/binary.ts b/drizzle-orm/src/googlesql/columns/binary.ts new file mode 100644 index 0000000000..b0f7001ea7 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/binary.ts @@ -0,0 +1,69 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlBinaryBuilderInitial = GoogleSqlBinaryBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlBinary'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class GoogleSqlBinaryBuilder> + extends GoogleSqlColumnBuilder< + T, + GoogleSqlBinaryConfig + > +{ + static override readonly [entityKind]: string = 'GoogleSqlBinaryBuilder'; + + constructor(name: T['name'], length: number | undefined) { + super(name, 'string', 'GoogleSqlBinary'); + this.config.length = length; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlBinary> { + return new GoogleSqlBinary>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlBinary> extends GoogleSqlColumn< + T, + GoogleSqlBinaryConfig +> { + static override readonly [entityKind]: string = 'GoogleSqlBinary'; + + length: number | undefined = this.config.length; + + getSQLType(): string { + return this.length === undefined ? `binary` : `binary(${this.length})`; + } +} + +export interface GoogleSqlBinaryConfig { + length?: number; +} + +export function binary(): GoogleSqlBinaryBuilderInitial<''>; +export function binary( + config?: GoogleSqlBinaryConfig, +): GoogleSqlBinaryBuilderInitial<''>; +export function binary( + name: TName, + config?: GoogleSqlBinaryConfig, +): GoogleSqlBinaryBuilderInitial; +export function binary(a?: string | GoogleSqlBinaryConfig, b: GoogleSqlBinaryConfig = {}) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlBinaryBuilder(name, config.length); +} diff --git a/drizzle-orm/src/googlesql/columns/boolean.ts b/drizzle-orm/src/googlesql/columns/boolean.ts new file mode 100644 index 0000000000..7f75796ff9 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/boolean.ts @@ -0,0 +1,55 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlBooleanBuilderInitial = GoogleSqlBooleanBuilder<{ + name: TName; + dataType: 'boolean'; + columnType: 'GoogleSqlBoolean'; + data: boolean; + driverParam: number | boolean; + enumValues: undefined; +}>; + +export class GoogleSqlBooleanBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlBooleanBuilder'; + + constructor(name: T['name']) { + super(name, 'boolean', 'GoogleSqlBoolean'); + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlBoolean> { + return new GoogleSqlBoolean>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlBoolean> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlBoolean'; + + getSQLType(): string { + return 'boolean'; + } + + override mapFromDriverValue(value: number | boolean): boolean { + if (typeof value === 'boolean') { + return value; + } + return value === 1; + } +} + +export function boolean(): GoogleSqlBooleanBuilderInitial<''>; +export function boolean(name: TName): GoogleSqlBooleanBuilderInitial; +export function boolean(name?: string) { + return new GoogleSqlBooleanBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/googlesql/columns/char.ts b/drizzle-orm/src/googlesql/columns/char.ts new file mode 100644 index 0000000000..54749c9ff6 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/char.ts @@ -0,0 +1,85 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig, type Writable } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlCharBuilderInitial< + TName extends string, + TEnum extends [string, ...string[]], + TLength extends number | undefined, +> = GoogleSqlCharBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlChar'; + data: TEnum[number]; + driverParam: number | string; + enumValues: TEnum; + length: TLength; +}>; + +export class GoogleSqlCharBuilder< + T extends ColumnBuilderBaseConfig<'string', 'GoogleSqlChar'> & { length?: number | undefined }, +> extends GoogleSqlColumnBuilder< + T, + GoogleSqlCharConfig, + { length: T['length'] } +> { + static override readonly [entityKind]: string = 'GoogleSqlCharBuilder'; + + constructor(name: T['name'], config: GoogleSqlCharConfig) { + super(name, 'string', 'GoogleSqlChar'); + this.config.length = config.length; + this.config.enum = config.enum; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlChar & { length: T['length']; enumValues: T['enumValues'] }> { + return new GoogleSqlChar & { length: T['length']; enumValues: T['enumValues'] }>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlChar & { length?: number | undefined }> + extends GoogleSqlColumn, { length: T['length'] }> +{ + static override readonly [entityKind]: string = 'GoogleSqlChar'; + + readonly length: T['length'] = this.config.length; + override readonly enumValues = this.config.enum; + + getSQLType(): string { + return this.length === undefined ? `char` : `char(${this.length})`; + } +} + +export interface GoogleSqlCharConfig< + TEnum extends readonly string[] | string[] | undefined = readonly string[] | string[] | undefined, + TLength extends number | undefined = number | undefined, +> { + enum?: TEnum; + length?: TLength; +} + +export function char(): GoogleSqlCharBuilderInitial<'', [string, ...string[]], undefined>; +export function char, L extends number | undefined>( + config?: GoogleSqlCharConfig, L>, +): GoogleSqlCharBuilderInitial<'', Writable, L>; +export function char< + TName extends string, + U extends string, + T extends Readonly<[U, ...U[]]>, + L extends number | undefined, +>( + name: TName, + config?: GoogleSqlCharConfig, L>, +): GoogleSqlCharBuilderInitial, L>; +export function char(a?: string | GoogleSqlCharConfig, b: GoogleSqlCharConfig = {}): any { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlCharBuilder(name, config as any); +} diff --git a/drizzle-orm/src/googlesql/columns/common.ts b/drizzle-orm/src/googlesql/columns/common.ts new file mode 100644 index 0000000000..eedcba8979 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/common.ts @@ -0,0 +1,155 @@ +import { ColumnBuilder } from '~/column-builder.ts'; +import type { + ColumnBuilderBase, + ColumnBuilderBaseConfig, + ColumnBuilderExtraConfig, + ColumnBuilderRuntimeConfig, + ColumnDataType, + HasDefault, + HasGenerated, + IsAutoincrement, + MakeColumnConfig, +} from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { Column } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { ForeignKey, UpdateDeleteAction } from '~/googlesql/foreign-keys.ts'; +import { ForeignKeyBuilder } from '~/googlesql/foreign-keys.ts'; +import type { AnyGoogleSqlTable, GoogleSqlTable } from '~/googlesql/table.ts'; +import type { SQL } from '~/sql/sql.ts'; +import type { Update } from '~/utils.ts'; +import { uniqueKeyName } from '../unique-constraint.ts'; + +export interface ReferenceConfig { + ref: () => GoogleSqlColumn; + actions: { + onUpdate?: UpdateDeleteAction; + onDelete?: UpdateDeleteAction; + }; +} + +export interface GoogleSqlColumnBuilderBase< + T extends ColumnBuilderBaseConfig = ColumnBuilderBaseConfig, + TTypeConfig extends object = object, +> extends ColumnBuilderBase {} + +export interface GoogleSqlGeneratedColumnConfig { + mode?: 'virtual' | 'stored'; +} + +export abstract class GoogleSqlColumnBuilder< + T extends ColumnBuilderBaseConfig = ColumnBuilderBaseConfig & { + data: any; + }, + TRuntimeConfig extends object = object, + TTypeConfig extends object = object, + TExtraConfig extends ColumnBuilderExtraConfig = ColumnBuilderExtraConfig, +> extends ColumnBuilder + implements GoogleSqlColumnBuilderBase +{ + static override readonly [entityKind]: string = 'GoogleSqlColumnBuilder'; + + private foreignKeyConfigs: ReferenceConfig[] = []; + + references(ref: ReferenceConfig['ref'], actions: ReferenceConfig['actions'] = {}): this { + this.foreignKeyConfigs.push({ ref, actions }); + return this; + } + + unique(name?: string): this { + this.config.isUnique = true; + this.config.uniqueName = name; + return this; + } + + generatedAlwaysAs(as: SQL | T['data'] | (() => SQL), config?: GoogleSqlGeneratedColumnConfig): HasGenerated { + this.config.generated = { + as, + type: 'always', + mode: config?.mode ?? 'virtual', + }; + return this as any; + } + + /** @internal */ + buildForeignKeys(column: GoogleSqlColumn, table: GoogleSqlTable): ForeignKey[] { + return this.foreignKeyConfigs.map(({ ref, actions }) => { + return ((ref, actions) => { + const builder = new ForeignKeyBuilder(() => { + const foreignColumn = ref(); + return { columns: [column], foreignColumns: [foreignColumn] }; + }); + if (actions.onUpdate) { + builder.onUpdate(actions.onUpdate); + } + if (actions.onDelete) { + builder.onDelete(actions.onDelete); + } + return builder.build(table); + })(ref, actions); + }); + } + + /** @internal */ + abstract build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlColumn>; +} + +// To understand how to use `GoogleSqlColumn` and `AnyGoogleSqlColumn`, see `Column` and `AnyColumn` documentation. +export abstract class GoogleSqlColumn< + T extends ColumnBaseConfig = ColumnBaseConfig, + TRuntimeConfig extends object = {}, + TTypeConfig extends object = {}, +> extends Column { + static override readonly [entityKind]: string = 'GoogleSqlColumn'; + + constructor( + override readonly table: GoogleSqlTable, + config: ColumnBuilderRuntimeConfig, + ) { + if (!config.uniqueName) { + config.uniqueName = uniqueKeyName(table, [config.name]); + } + super(table, config); + } +} + +export type AnyGoogleSqlColumn> = {}> = + GoogleSqlColumn< + Required, TPartial>> + >; + +export interface GoogleSqlColumnWithAutoIncrementConfig { + autoIncrement: boolean; +} + +export abstract class GoogleSqlColumnBuilderWithAutoIncrement< + T extends ColumnBuilderBaseConfig = ColumnBuilderBaseConfig, + TRuntimeConfig extends object = object, + TExtraConfig extends ColumnBuilderExtraConfig = ColumnBuilderExtraConfig, +> extends GoogleSqlColumnBuilder { + static override readonly [entityKind]: string = 'GoogleSqlColumnBuilderWithAutoIncrement'; + + constructor(name: NonNullable, dataType: T['dataType'], columnType: T['columnType']) { + super(name, dataType, columnType); + this.config.autoIncrement = false; + } + + autoincrement(): IsAutoincrement> { + this.config.autoIncrement = true; + this.config.hasDefault = true; + return this as IsAutoincrement>; + } +} + +export abstract class GoogleSqlColumnWithAutoIncrement< + T extends ColumnBaseConfig = ColumnBaseConfig, + TRuntimeConfig extends object = object, +> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlColumnWithAutoIncrement'; + + readonly autoIncrement: boolean = this.config.autoIncrement; +} diff --git a/drizzle-orm/src/googlesql/columns/custom.ts b/drizzle-orm/src/googlesql/columns/custom.ts new file mode 100644 index 0000000000..8252bc7d73 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/custom.ts @@ -0,0 +1,234 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import type { SQL } from '~/sql/sql.ts'; +import { type Equal, getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type ConvertCustomConfig> = + & { + name: TName; + dataType: 'custom'; + columnType: 'GoogleSqlCustomColumn'; + data: T['data']; + driverParam: T['driverData']; + enumValues: undefined; + } + & (T['notNull'] extends true ? { notNull: true } : {}) + & (T['default'] extends true ? { hasDefault: true } : {}); + +export interface GoogleSqlCustomColumnInnerConfig { + customTypeValues: CustomTypeValues; +} + +export class GoogleSqlCustomColumnBuilder> + extends GoogleSqlColumnBuilder< + T, + { + fieldConfig: CustomTypeValues['config']; + customTypeParams: CustomTypeParams; + }, + { + googlesqlColumnBuilderBrand: 'GoogleSqlCustomColumnBuilderBrand'; + } + > +{ + static override readonly [entityKind]: string = 'GoogleSqlCustomColumnBuilder'; + + constructor( + name: T['name'], + fieldConfig: CustomTypeValues['config'], + customTypeParams: CustomTypeParams, + ) { + super(name, 'custom', 'GoogleSqlCustomColumn'); + this.config.fieldConfig = fieldConfig; + this.config.customTypeParams = customTypeParams; + } + + /** @internal */ + build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlCustomColumn> { + return new GoogleSqlCustomColumn>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlCustomColumn> + extends GoogleSqlColumn +{ + static override readonly [entityKind]: string = 'GoogleSqlCustomColumn'; + + private sqlName: string; + private mapTo?: (value: T['data']) => T['driverParam']; + private mapFrom?: (value: T['driverParam']) => T['data']; + + constructor( + table: AnyGoogleSqlTable<{ name: T['tableName'] }>, + config: GoogleSqlCustomColumnBuilder['config'], + ) { + super(table, config); + this.sqlName = config.customTypeParams.dataType(config.fieldConfig); + this.mapTo = config.customTypeParams.toDriver; + this.mapFrom = config.customTypeParams.fromDriver; + } + + getSQLType(): string { + return this.sqlName; + } + + override mapFromDriverValue(value: T['driverParam']): T['data'] { + return typeof this.mapFrom === 'function' ? this.mapFrom(value) : value as T['data']; + } + + override mapToDriverValue(value: T['data']): T['driverParam'] { + return typeof this.mapTo === 'function' ? this.mapTo(value) : value as T['data']; + } +} + +export type CustomTypeValues = { + /** + * Required type for custom column, that will infer proper type model + * + * Examples: + * + * If you want your column to be `string` type after selecting/or on inserting - use `data: string`. Like `text`, `varchar` + * + * If you want your column to be `number` type after selecting/or on inserting - use `data: number`. Like `integer` + */ + data: unknown; + + /** + * Type helper, that represents what type database driver is accepting for specific database data type + */ + driverData?: unknown; + + /** + * What config type should be used for {@link CustomTypeParams} `dataType` generation + */ + config?: Record; + + /** + * Whether the config argument should be required or not + * @default false + */ + configRequired?: boolean; + + /** + * If your custom data type should be notNull by default you can use `notNull: true` + * + * @example + * const customSerial = customType<{ data: number, notNull: true, default: true }>({ + * dataType() { + * return 'serial'; + * }, + * }); + */ + notNull?: boolean; + + /** + * If your custom data type has default you can use `default: true` + * + * @example + * const customSerial = customType<{ data: number, notNull: true, default: true }>({ + * dataType() { + * return 'serial'; + * }, + * }); + */ + default?: boolean; +}; + +export interface CustomTypeParams { + /** + * Database data type string representation, that is used for migrations + * @example + * ``` + * `jsonb`, `text` + * ``` + * + * If database data type needs additional params you can use them from `config` param + * @example + * ``` + * `varchar(256)`, `numeric(2,3)` + * ``` + * + * To make `config` be of specific type please use config generic in {@link CustomTypeValues} + * + * @example + * Usage example + * ``` + * dataType() { + * return 'boolean'; + * }, + * ``` + * Or + * ``` + * dataType(config) { + * return typeof config.length !== 'undefined' ? `varchar(${config.length})` : `varchar`; + * } + * ``` + */ + dataType: (config: T['config'] | (Equal extends true ? never : undefined)) => string; + + /** + * Optional mapping function, between user input and driver + * @example + * For example, when using jsonb we need to map JS/TS object to string before writing to database + * ``` + * toDriver(value: TData): string { + * return JSON.stringify(value); + * } + * ``` + */ + toDriver?: (value: T['data']) => T['driverData'] | SQL; + + /** + * Optional mapping function, that is responsible for data mapping from database to JS/TS code + * @example + * For example, when using timestamp we need to map string Date representation to JS Date + * ``` + * fromDriver(value: string): Date { + * return new Date(value); + * }, + * ``` + */ + fromDriver?: (value: T['driverData']) => T['data']; +} + +/** + * Custom googlesql database data type generator + */ +export function customType( + customTypeParams: CustomTypeParams, +): Equal extends true ? { + & T['config']>( + fieldConfig: TConfig, + ): GoogleSqlCustomColumnBuilder>; + ( + dbName: TName, + fieldConfig: T['config'], + ): GoogleSqlCustomColumnBuilder>; + } + : { + (): GoogleSqlCustomColumnBuilder>; + & T['config']>( + fieldConfig?: TConfig, + ): GoogleSqlCustomColumnBuilder>; + ( + dbName: TName, + fieldConfig?: T['config'], + ): GoogleSqlCustomColumnBuilder>; + } +{ + return ( + a?: TName | T['config'], + b?: T['config'], + ): GoogleSqlCustomColumnBuilder> => { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlCustomColumnBuilder(name as ConvertCustomConfig['name'], config, customTypeParams); + }; +} diff --git a/drizzle-orm/src/googlesql/columns/date.common.ts b/drizzle-orm/src/googlesql/columns/date.common.ts new file mode 100644 index 0000000000..26397bb4bd --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/date.common.ts @@ -0,0 +1,42 @@ +import type { + ColumnBuilderBaseConfig, + ColumnBuilderExtraConfig, + ColumnDataType, + HasDefault, +} from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { sql } from '~/sql/sql.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export interface GoogleSqlDateColumnBaseConfig { + hasOnUpdateNow: boolean; +} + +export abstract class GoogleSqlDateColumnBaseBuilder< + T extends ColumnBuilderBaseConfig, + TRuntimeConfig extends object = object, + TExtraConfig extends ColumnBuilderExtraConfig = ColumnBuilderExtraConfig, +> extends GoogleSqlColumnBuilder { + static override readonly [entityKind]: string = 'GoogleSqlDateColumnBuilder'; + + defaultNow() { + return this.default(sql`(now())`); + } + + // "on update now" also adds an implicit default value to the column - https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html + onUpdateNow(): HasDefault { + this.config.hasOnUpdateNow = true; + this.config.hasDefault = true; + return this as HasDefault; + } +} + +export abstract class GoogleSqlDateBaseColumn< + T extends ColumnBaseConfig, + TRuntimeConfig extends object = object, +> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlDateColumn'; + + readonly hasOnUpdateNow: boolean = this.config.hasOnUpdateNow; +} diff --git a/drizzle-orm/src/googlesql/columns/date.ts b/drizzle-orm/src/googlesql/columns/date.ts new file mode 100644 index 0000000000..1bffecaeb0 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/date.ts @@ -0,0 +1,120 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { type Equal, getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlDateBuilderInitial = GoogleSqlDateBuilder<{ + name: TName; + dataType: 'date'; + columnType: 'GoogleSqlDate'; + data: Date; + driverParam: string | number; + enumValues: undefined; +}>; + +export class GoogleSqlDateBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlDateBuilder'; + + constructor(name: T['name']) { + super(name, 'date', 'GoogleSqlDate'); + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlDate> { + return new GoogleSqlDate>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlDate> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlDate'; + + constructor( + table: AnyGoogleSqlTable<{ name: T['tableName'] }>, + config: GoogleSqlDateBuilder['config'], + ) { + super(table, config); + } + + getSQLType(): string { + return `date`; + } + + override mapFromDriverValue(value: string): Date { + return new Date(value); + } +} + +export type GoogleSqlDateStringBuilderInitial = GoogleSqlDateStringBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlDateString'; + data: string; + driverParam: string | number; + enumValues: undefined; +}>; + +export class GoogleSqlDateStringBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlDateStringBuilder'; + + constructor(name: T['name']) { + super(name, 'string', 'GoogleSqlDateString'); + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlDateString> { + return new GoogleSqlDateString>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlDateString> + extends GoogleSqlColumn +{ + static override readonly [entityKind]: string = 'GoogleSqlDateString'; + + constructor( + table: AnyGoogleSqlTable<{ name: T['tableName'] }>, + config: GoogleSqlDateStringBuilder['config'], + ) { + super(table, config); + } + + getSQLType(): string { + return `date`; + } +} + +export interface GoogleSqlDateConfig { + mode?: TMode; +} + +export function date(): GoogleSqlDateBuilderInitial<''>; +export function date( + config?: GoogleSqlDateConfig, +): Equal extends true ? GoogleSqlDateStringBuilderInitial<''> : GoogleSqlDateBuilderInitial<''>; +export function date( + name: TName, + config?: GoogleSqlDateConfig, +): Equal extends true ? GoogleSqlDateStringBuilderInitial : GoogleSqlDateBuilderInitial; +export function date(a?: string | GoogleSqlDateConfig, b?: GoogleSqlDateConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'string') { + return new GoogleSqlDateStringBuilder(name); + } + return new GoogleSqlDateBuilder(name); +} diff --git a/drizzle-orm/src/googlesql/columns/datetime.ts b/drizzle-orm/src/googlesql/columns/datetime.ts new file mode 100644 index 0000000000..81281beeee --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/datetime.ts @@ -0,0 +1,139 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { type Equal, getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlDateTimeBuilderInitial = GoogleSqlDateTimeBuilder<{ + name: TName; + dataType: 'date'; + columnType: 'GoogleSqlDateTime'; + data: Date; + driverParam: string | number; + enumValues: undefined; +}>; + +export class GoogleSqlDateTimeBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlDateTimeBuilder'; + + constructor(name: T['name'], config: GoogleSqlDatetimeConfig | undefined) { + super(name, 'date', 'GoogleSqlDateTime'); + this.config.fsp = config?.fsp; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlDateTime> { + return new GoogleSqlDateTime>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlDateTime> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlDateTime'; + + readonly fsp: number | undefined; + + constructor( + table: AnyGoogleSqlTable<{ name: T['tableName'] }>, + config: GoogleSqlDateTimeBuilder['config'], + ) { + super(table, config); + this.fsp = config.fsp; + } + + getSQLType(): string { + const precision = this.fsp === undefined ? '' : `(${this.fsp})`; + return `datetime${precision}`; + } + + override mapToDriverValue(value: Date): unknown { + return value.toISOString().replace('T', ' ').replace('Z', ''); + } + + override mapFromDriverValue(value: string): Date { + return new Date(value.replace(' ', 'T') + 'Z'); + } +} + +export type GoogleSqlDateTimeStringBuilderInitial = GoogleSqlDateTimeStringBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlDateTimeString'; + data: string; + driverParam: string | number; + enumValues: undefined; +}>; + +export class GoogleSqlDateTimeStringBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlDateTimeStringBuilder'; + + constructor(name: T['name'], config: GoogleSqlDatetimeConfig | undefined) { + super(name, 'string', 'GoogleSqlDateTimeString'); + this.config.fsp = config?.fsp; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlDateTimeString> { + return new GoogleSqlDateTimeString>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlDateTimeString> + extends GoogleSqlColumn +{ + static override readonly [entityKind]: string = 'GoogleSqlDateTimeString'; + + readonly fsp: number | undefined; + + constructor( + table: AnyGoogleSqlTable<{ name: T['tableName'] }>, + config: GoogleSqlDateTimeStringBuilder['config'], + ) { + super(table, config); + this.fsp = config.fsp; + } + + getSQLType(): string { + const precision = this.fsp === undefined ? '' : `(${this.fsp})`; + return `datetime${precision}`; + } +} + +export type DatetimeFsp = 0 | 1 | 2 | 3 | 4 | 5 | 6; + +export interface GoogleSqlDatetimeConfig { + mode?: TMode; + fsp?: DatetimeFsp; +} + +export function datetime(): GoogleSqlDateTimeBuilderInitial<''>; +export function datetime( + config?: GoogleSqlDatetimeConfig, +): Equal extends true ? GoogleSqlDateTimeStringBuilderInitial<''> + : GoogleSqlDateTimeBuilderInitial<''>; +export function datetime( + name: TName, + config?: GoogleSqlDatetimeConfig, +): Equal extends true ? GoogleSqlDateTimeStringBuilderInitial + : GoogleSqlDateTimeBuilderInitial; +export function datetime(a?: string | GoogleSqlDatetimeConfig, b?: GoogleSqlDatetimeConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'string') { + return new GoogleSqlDateTimeStringBuilder(name, config); + } + return new GoogleSqlDateTimeBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/decimal.ts b/drizzle-orm/src/googlesql/columns/decimal.ts new file mode 100644 index 0000000000..0738a3afc9 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/decimal.ts @@ -0,0 +1,80 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; + +export type GoogleSqlDecimalBuilderInitial = GoogleSqlDecimalBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlDecimal'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class GoogleSqlDecimalBuilder< + T extends ColumnBuilderBaseConfig<'string', 'GoogleSqlDecimal'>, +> extends GoogleSqlColumnBuilderWithAutoIncrement { + static override readonly [entityKind]: string = 'GoogleSqlDecimalBuilder'; + + constructor(name: T['name'], config: GoogleSqlDecimalConfig | undefined) { + super(name, 'string', 'GoogleSqlDecimal'); + this.config.precision = config?.precision; + this.config.scale = config?.scale; + this.config.unsigned = config?.unsigned; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlDecimal> { + return new GoogleSqlDecimal>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlDecimal> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlDecimal'; + + readonly precision: number | undefined = this.config.precision; + readonly scale: number | undefined = this.config.scale; + readonly unsigned: boolean | undefined = this.config.unsigned; + + getSQLType(): string { + let type = ''; + if (this.precision !== undefined && this.scale !== undefined) { + type += `decimal(${this.precision},${this.scale})`; + } else if (this.precision === undefined) { + type += 'decimal'; + } else { + type += `decimal(${this.precision})`; + } + type = type === 'decimal(10,0)' || type === 'decimal(10)' ? 'decimal' : type; + return this.unsigned ? `${type} unsigned` : type; + } +} + +export interface GoogleSqlDecimalConfig { + precision?: number; + scale?: number; + unsigned?: boolean; +} + +export function decimal(): GoogleSqlDecimalBuilderInitial<''>; +export function decimal( + config: GoogleSqlDecimalConfig, +): GoogleSqlDecimalBuilderInitial<''>; +export function decimal( + name: TName, + config?: GoogleSqlDecimalConfig, +): GoogleSqlDecimalBuilderInitial; +export function decimal(a?: string | GoogleSqlDecimalConfig, b: GoogleSqlDecimalConfig = {}) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlDecimalBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/double.ts b/drizzle-orm/src/googlesql/columns/double.ts new file mode 100644 index 0000000000..01d4f372df --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/double.ts @@ -0,0 +1,79 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; + +export type GoogleSqlDoubleBuilderInitial = GoogleSqlDoubleBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlDouble'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class GoogleSqlDoubleBuilder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlDoubleBuilder'; + + constructor(name: T['name'], config: GoogleSqlDoubleConfig | undefined) { + super(name, 'number', 'GoogleSqlDouble'); + this.config.precision = config?.precision; + this.config.scale = config?.scale; + this.config.unsigned = config?.unsigned; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlDouble> { + return new GoogleSqlDouble>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlDouble> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlDouble'; + + readonly precision: number | undefined = this.config.precision; + readonly scale: number | undefined = this.config.scale; + readonly unsigned: boolean | undefined = this.config.unsigned; + + getSQLType(): string { + let type = ''; + if (this.precision !== undefined && this.scale !== undefined) { + type += `double(${this.precision},${this.scale})`; + } else if (this.precision === undefined) { + type += 'double'; + } else { + type += `double(${this.precision})`; + } + return this.unsigned ? `${type} unsigned` : type; + } +} + +export interface GoogleSqlDoubleConfig { + precision?: number; + scale?: number; + unsigned?: boolean; +} + +export function double(): GoogleSqlDoubleBuilderInitial<''>; +export function double( + config?: GoogleSqlDoubleConfig, +): GoogleSqlDoubleBuilderInitial<''>; +export function double( + name: TName, + config?: GoogleSqlDoubleConfig, +): GoogleSqlDoubleBuilderInitial; +export function double(a?: string | GoogleSqlDoubleConfig, b?: GoogleSqlDoubleConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlDoubleBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/enum.ts b/drizzle-orm/src/googlesql/columns/enum.ts new file mode 100644 index 0000000000..126a62fe53 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/enum.ts @@ -0,0 +1,69 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig, type Writable } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlEnumColumnBuilderInitial = + GoogleSqlEnumColumnBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlEnumColumn'; + data: TEnum[number]; + driverParam: string; + enumValues: TEnum; + }>; + +export class GoogleSqlEnumColumnBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlEnumColumnBuilder'; + + constructor(name: T['name'], values: T['enumValues']) { + super(name, 'string', 'GoogleSqlEnumColumn'); + this.config.enumValues = values; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlEnumColumn & { enumValues: T['enumValues'] }> { + return new GoogleSqlEnumColumn & { enumValues: T['enumValues'] }>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlEnumColumn> + extends GoogleSqlColumn +{ + static override readonly [entityKind]: string = 'GoogleSqlEnumColumn'; + + override readonly enumValues = this.config.enumValues; + + getSQLType(): string { + return `enum(${this.enumValues!.map((value) => `'${value}'`).join(',')})`; + } +} + +export function googlesqlEnum>( + values: T | Writable, +): GoogleSqlEnumColumnBuilderInitial<'', Writable>; +export function googlesqlEnum>( + name: TName, + values: T | Writable, +): GoogleSqlEnumColumnBuilderInitial>; +export function googlesqlEnum( + a?: string | readonly [string, ...string[]] | [string, ...string[]], + b?: readonly [string, ...string[]] | [string, ...string[]], +): any { + const { name, config: values } = getColumnNameAndConfig(a, b); + + if (values.length === 0) { + throw new Error(`You have an empty array for "${name}" enum values`); + } + + return new GoogleSqlEnumColumnBuilder(name, values as any); +} diff --git a/drizzle-orm/src/googlesql/columns/float.ts b/drizzle-orm/src/googlesql/columns/float.ts new file mode 100644 index 0000000000..a37e10cd0c --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/float.ts @@ -0,0 +1,79 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; + +export type GoogleSqlFloatBuilderInitial = GoogleSqlFloatBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlFloat'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class GoogleSqlFloatBuilder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlFloatBuilder'; + + constructor(name: T['name'], config: GoogleSqlFloatConfig | undefined) { + super(name, 'number', 'GoogleSqlFloat'); + this.config.precision = config?.precision; + this.config.scale = config?.scale; + this.config.unsigned = config?.unsigned; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlFloat> { + return new GoogleSqlFloat>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlFloat> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlFloat'; + + readonly precision: number | undefined = this.config.precision; + readonly scale: number | undefined = this.config.scale; + readonly unsigned: boolean | undefined = this.config.unsigned; + + getSQLType(): string { + let type = ''; + if (this.precision !== undefined && this.scale !== undefined) { + type += `float(${this.precision},${this.scale})`; + } else if (this.precision === undefined) { + type += 'float'; + } else { + type += `float(${this.precision})`; + } + return this.unsigned ? `${type} unsigned` : type; + } +} + +export interface GoogleSqlFloatConfig { + precision?: number; + scale?: number; + unsigned?: boolean; +} + +export function float(): GoogleSqlFloatBuilderInitial<''>; +export function float( + config?: GoogleSqlFloatConfig, +): GoogleSqlFloatBuilderInitial<''>; +export function float( + name: TName, + config?: GoogleSqlFloatConfig, +): GoogleSqlFloatBuilderInitial; +export function float(a?: string | GoogleSqlFloatConfig, b?: GoogleSqlFloatConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlFloatBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/index.ts b/drizzle-orm/src/googlesql/columns/index.ts new file mode 100644 index 0000000000..b51f0fac48 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/index.ts @@ -0,0 +1,25 @@ +export * from './bigint.ts'; +export * from './binary.ts'; +export * from './boolean.ts'; +export * from './char.ts'; +export * from './common.ts'; +export * from './custom.ts'; +export * from './date.ts'; +export * from './datetime.ts'; +export * from './decimal.ts'; +export * from './double.ts'; +export * from './enum.ts'; +export * from './float.ts'; +export * from './int.ts'; +export * from './json.ts'; +export * from './mediumint.ts'; +export * from './real.ts'; +export * from './serial.ts'; +export * from './smallint.ts'; +export * from './text.ts'; +export * from './time.ts'; +export * from './timestamp.ts'; +export * from './tinyint.ts'; +export * from './varbinary.ts'; +export * from './varchar.ts'; +export * from './year.ts'; diff --git a/drizzle-orm/src/googlesql/columns/int.ts b/drizzle-orm/src/googlesql/columns/int.ts new file mode 100644 index 0000000000..fe143c8d34 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/int.ts @@ -0,0 +1,70 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; + +export type GoogleSqlIntBuilderInitial = GoogleSqlIntBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlInt'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class GoogleSqlIntBuilder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlIntBuilder'; + + constructor(name: T['name'], config?: GoogleSqlIntConfig) { + super(name, 'number', 'GoogleSqlInt'); + this.config.unsigned = config ? config.unsigned : false; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlInt> { + return new GoogleSqlInt>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlInt> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlInt'; + + getSQLType(): string { + return `int${this.config.unsigned ? ' unsigned' : ''}`; + } + + override mapFromDriverValue(value: number | string): number { + if (typeof value === 'string') { + return Number(value); + } + return value; + } +} + +export interface GoogleSqlIntConfig { + unsigned?: boolean; +} + +export function int(): GoogleSqlIntBuilderInitial<''>; +export function int( + config?: GoogleSqlIntConfig, +): GoogleSqlIntBuilderInitial<''>; +export function int( + name: TName, + config?: GoogleSqlIntConfig, +): GoogleSqlIntBuilderInitial; +export function int(a?: string | GoogleSqlIntConfig, b?: GoogleSqlIntConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlIntBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/json.ts b/drizzle-orm/src/googlesql/columns/json.ts new file mode 100644 index 0000000000..33af18af1f --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/json.ts @@ -0,0 +1,52 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlJsonBuilderInitial = GoogleSqlJsonBuilder<{ + name: TName; + dataType: 'json'; + columnType: 'GoogleSqlJson'; + data: unknown; + driverParam: string; + enumValues: undefined; +}>; + +export class GoogleSqlJsonBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlJsonBuilder'; + + constructor(name: T['name']) { + super(name, 'json', 'GoogleSqlJson'); + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlJson> { + return new GoogleSqlJson>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlJson> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlJson'; + + getSQLType(): string { + return 'json'; + } + + override mapToDriverValue(value: T['data']): string { + return JSON.stringify(value); + } +} + +export function json(): GoogleSqlJsonBuilderInitial<''>; +export function json(name: TName): GoogleSqlJsonBuilderInitial; +export function json(name?: string) { + return new GoogleSqlJsonBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/googlesql/columns/mediumint.ts b/drizzle-orm/src/googlesql/columns/mediumint.ts new file mode 100644 index 0000000000..915eb47916 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/mediumint.ts @@ -0,0 +1,67 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; +import type { GoogleSqlIntConfig } from './int.ts'; + +export type GoogleSqlMediumIntBuilderInitial = GoogleSqlMediumIntBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlMediumInt'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class GoogleSqlMediumIntBuilder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlMediumIntBuilder'; + + constructor(name: T['name'], config?: GoogleSqlIntConfig) { + super(name, 'number', 'GoogleSqlMediumInt'); + this.config.unsigned = config ? config.unsigned : false; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlMediumInt> { + return new GoogleSqlMediumInt>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlMediumInt> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlMediumInt'; + + getSQLType(): string { + return `mediumint${this.config.unsigned ? ' unsigned' : ''}`; + } + + override mapFromDriverValue(value: number | string): number { + if (typeof value === 'string') { + return Number(value); + } + return value; + } +} + +export function mediumint(): GoogleSqlMediumIntBuilderInitial<''>; +export function mediumint( + config?: GoogleSqlIntConfig, +): GoogleSqlMediumIntBuilderInitial<''>; +export function mediumint( + name: TName, + config?: GoogleSqlIntConfig, +): GoogleSqlMediumIntBuilderInitial; +export function mediumint(a?: string | GoogleSqlIntConfig, b?: GoogleSqlIntConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlMediumIntBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/real.ts b/drizzle-orm/src/googlesql/columns/real.ts new file mode 100644 index 0000000000..87443b07a3 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/real.ts @@ -0,0 +1,80 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; + +export type GoogleSqlRealBuilderInitial = GoogleSqlRealBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlReal'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class GoogleSqlRealBuilder> + extends GoogleSqlColumnBuilderWithAutoIncrement< + T, + GoogleSqlRealConfig + > +{ + static override readonly [entityKind]: string = 'GoogleSqlRealBuilder'; + + constructor(name: T['name'], config: GoogleSqlRealConfig | undefined) { + super(name, 'number', 'GoogleSqlReal'); + this.config.precision = config?.precision; + this.config.scale = config?.scale; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlReal> { + return new GoogleSqlReal>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlReal> + extends GoogleSqlColumnWithAutoIncrement< + T, + GoogleSqlRealConfig + > +{ + static override readonly [entityKind]: string = 'GoogleSqlReal'; + + precision: number | undefined = this.config.precision; + scale: number | undefined = this.config.scale; + + getSQLType(): string { + if (this.precision !== undefined && this.scale !== undefined) { + return `real(${this.precision}, ${this.scale})`; + } else if (this.precision === undefined) { + return 'real'; + } else { + return `real(${this.precision})`; + } + } +} + +export interface GoogleSqlRealConfig { + precision?: number; + scale?: number; +} + +export function real(): GoogleSqlRealBuilderInitial<''>; +export function real( + config?: GoogleSqlRealConfig, +): GoogleSqlRealBuilderInitial<''>; +export function real( + name: TName, + config?: GoogleSqlRealConfig, +): GoogleSqlRealBuilderInitial; +export function real(a?: string | GoogleSqlRealConfig, b: GoogleSqlRealConfig = {}) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlRealBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/serial.ts b/drizzle-orm/src/googlesql/columns/serial.ts new file mode 100644 index 0000000000..7371b68c56 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/serial.ts @@ -0,0 +1,75 @@ +import type { + ColumnBuilderBaseConfig, + ColumnBuilderRuntimeConfig, + HasDefault, + IsAutoincrement, + IsPrimaryKey, + MakeColumnConfig, + NotNull, +} from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; + +export type GoogleSqlSerialBuilderInitial = IsAutoincrement< + IsPrimaryKey< + NotNull< + HasDefault< + GoogleSqlSerialBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlSerial'; + data: number; + driverParam: number; + enumValues: undefined; + }> + > + > + > +>; + +export class GoogleSqlSerialBuilder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlSerialBuilder'; + + constructor(name: T['name']) { + super(name, 'number', 'GoogleSqlSerial'); + this.config.hasDefault = true; + this.config.autoIncrement = true; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlSerial> { + return new GoogleSqlSerial>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlSerial< + T extends ColumnBaseConfig<'number', 'GoogleSqlSerial'>, +> extends GoogleSqlColumnWithAutoIncrement { + static override readonly [entityKind]: string = 'GoogleSqlSerial'; + + getSQLType(): string { + return 'serial'; + } + + override mapFromDriverValue(value: number | string): number { + if (typeof value === 'string') { + return Number(value); + } + return value; + } +} + +export function serial(): GoogleSqlSerialBuilderInitial<''>; +export function serial(name: TName): GoogleSqlSerialBuilderInitial; +export function serial(name?: string) { + return new GoogleSqlSerialBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/googlesql/columns/smallint.ts b/drizzle-orm/src/googlesql/columns/smallint.ts new file mode 100644 index 0000000000..81516580f1 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/smallint.ts @@ -0,0 +1,67 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; +import type { GoogleSqlIntConfig } from './int.ts'; + +export type GoogleSqlSmallIntBuilderInitial = GoogleSqlSmallIntBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlSmallInt'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class GoogleSqlSmallIntBuilder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlSmallIntBuilder'; + + constructor(name: T['name'], config?: GoogleSqlIntConfig) { + super(name, 'number', 'GoogleSqlSmallInt'); + this.config.unsigned = config ? config.unsigned : false; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlSmallInt> { + return new GoogleSqlSmallInt>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlSmallInt> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlSmallInt'; + + getSQLType(): string { + return `smallint${this.config.unsigned ? ' unsigned' : ''}`; + } + + override mapFromDriverValue(value: number | string): number { + if (typeof value === 'string') { + return Number(value); + } + return value; + } +} + +export function smallint(): GoogleSqlSmallIntBuilderInitial<''>; +export function smallint( + config?: GoogleSqlIntConfig, +): GoogleSqlSmallIntBuilderInitial<''>; +export function smallint( + name: TName, + config?: GoogleSqlIntConfig, +): GoogleSqlSmallIntBuilderInitial; +export function smallint(a?: string | GoogleSqlIntConfig, b?: GoogleSqlIntConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlSmallIntBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/text.ts b/drizzle-orm/src/googlesql/columns/text.ts new file mode 100644 index 0000000000..bb4d83a704 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/text.ts @@ -0,0 +1,115 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig, type Writable } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlTextColumnType = 'tinytext' | 'text' | 'mediumtext' | 'longtext'; + +export type GoogleSqlTextBuilderInitial = + GoogleSqlTextBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlText'; + data: TEnum[number]; + driverParam: string; + enumValues: TEnum; + }>; + +export class GoogleSqlTextBuilder> + extends GoogleSqlColumnBuilder< + T, + { textType: GoogleSqlTextColumnType; enumValues: T['enumValues'] } + > +{ + static override readonly [entityKind]: string = 'GoogleSqlTextBuilder'; + + constructor(name: T['name'], textType: GoogleSqlTextColumnType, config: GoogleSqlTextConfig) { + super(name, 'string', 'GoogleSqlText'); + this.config.textType = textType; + this.config.enumValues = config.enum; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlText> { + return new GoogleSqlText>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlText> + extends GoogleSqlColumn +{ + static override readonly [entityKind]: string = 'GoogleSqlText'; + + readonly textType: GoogleSqlTextColumnType = this.config.textType; + + override readonly enumValues = this.config.enumValues; + + getSQLType(): string { + return this.textType; + } +} + +export interface GoogleSqlTextConfig< + TEnum extends readonly string[] | string[] | undefined = readonly string[] | string[] | undefined, +> { + enum?: TEnum; +} + +export function text(): GoogleSqlTextBuilderInitial<'', [string, ...string[]]>; +export function text>( + config?: GoogleSqlTextConfig>, +): GoogleSqlTextBuilderInitial<'', Writable>; +export function text>( + name: TName, + config?: GoogleSqlTextConfig>, +): GoogleSqlTextBuilderInitial>; +export function text(a?: string | GoogleSqlTextConfig, b: GoogleSqlTextConfig = {}): any { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlTextBuilder(name, 'text', config as any); +} + +export function tinytext(): GoogleSqlTextBuilderInitial<'', [string, ...string[]]>; +export function tinytext>( + config?: GoogleSqlTextConfig>, +): GoogleSqlTextBuilderInitial<'', Writable>; +export function tinytext>( + name: TName, + config?: GoogleSqlTextConfig>, +): GoogleSqlTextBuilderInitial>; +export function tinytext(a?: string | GoogleSqlTextConfig, b: GoogleSqlTextConfig = {}): any { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlTextBuilder(name, 'tinytext', config as any); +} + +export function mediumtext(): GoogleSqlTextBuilderInitial<'', [string, ...string[]]>; +export function mediumtext>( + config?: GoogleSqlTextConfig>, +): GoogleSqlTextBuilderInitial<'', Writable>; +export function mediumtext>( + name: TName, + config?: GoogleSqlTextConfig>, +): GoogleSqlTextBuilderInitial>; +export function mediumtext(a?: string | GoogleSqlTextConfig, b: GoogleSqlTextConfig = {}): any { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlTextBuilder(name, 'mediumtext', config as any); +} + +export function longtext(): GoogleSqlTextBuilderInitial<'', [string, ...string[]]>; +export function longtext>( + config?: GoogleSqlTextConfig>, +): GoogleSqlTextBuilderInitial<'', Writable>; +export function longtext>( + name: TName, + config?: GoogleSqlTextConfig>, +): GoogleSqlTextBuilderInitial>; +export function longtext(a?: string | GoogleSqlTextConfig, b: GoogleSqlTextConfig = {}): any { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlTextBuilder(name, 'longtext', config as any); +} diff --git a/drizzle-orm/src/googlesql/columns/time.ts b/drizzle-orm/src/googlesql/columns/time.ts new file mode 100644 index 0000000000..89f22e51d4 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/time.ts @@ -0,0 +1,72 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlTimeBuilderInitial = GoogleSqlTimeBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlTime'; + data: string; + driverParam: string | number; + enumValues: undefined; +}>; + +export class GoogleSqlTimeBuilder> + extends GoogleSqlColumnBuilder< + T, + TimeConfig + > +{ + static override readonly [entityKind]: string = 'GoogleSqlTimeBuilder'; + + constructor( + name: T['name'], + config: TimeConfig | undefined, + ) { + super(name, 'string', 'GoogleSqlTime'); + this.config.fsp = config?.fsp; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlTime> { + return new GoogleSqlTime>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlTime< + T extends ColumnBaseConfig<'string', 'GoogleSqlTime'>, +> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlTime'; + + readonly fsp: number | undefined = this.config.fsp; + + getSQLType(): string { + const precision = this.fsp === undefined ? '' : `(${this.fsp})`; + return `time${precision}`; + } +} + +export type TimeConfig = { + fsp?: 0 | 1 | 2 | 3 | 4 | 5 | 6; +}; + +export function time(): GoogleSqlTimeBuilderInitial<''>; +export function time( + config?: TimeConfig, +): GoogleSqlTimeBuilderInitial<''>; +export function time( + name: TName, + config?: TimeConfig, +): GoogleSqlTimeBuilderInitial; +export function time(a?: string | TimeConfig, b?: TimeConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlTimeBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/timestamp.ts b/drizzle-orm/src/googlesql/columns/timestamp.ts new file mode 100644 index 0000000000..e8a4795aa0 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/timestamp.ts @@ -0,0 +1,125 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { type Equal, getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlDateBaseColumn, GoogleSqlDateColumnBaseBuilder } from './date.common.ts'; + +export type GoogleSqlTimestampBuilderInitial = GoogleSqlTimestampBuilder<{ + name: TName; + dataType: 'date'; + columnType: 'GoogleSqlTimestamp'; + data: Date; + driverParam: string | number; + enumValues: undefined; +}>; + +export class GoogleSqlTimestampBuilder> + extends GoogleSqlDateColumnBaseBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlTimestampBuilder'; + + constructor(name: T['name'], config: GoogleSqlTimestampConfig | undefined) { + super(name, 'date', 'GoogleSqlTimestamp'); + this.config.fsp = config?.fsp; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlTimestamp> { + return new GoogleSqlTimestamp>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlTimestamp> + extends GoogleSqlDateBaseColumn +{ + static override readonly [entityKind]: string = 'GoogleSqlTimestamp'; + + readonly fsp: number | undefined = this.config.fsp; + + getSQLType(): string { + const precision = this.fsp === undefined ? '' : `(${this.fsp})`; + return `timestamp${precision}`; + } + + override mapFromDriverValue(value: string): Date { + return new Date(value + '+0000'); + } + + override mapToDriverValue(value: Date): string { + return value.toISOString().slice(0, -1).replace('T', ' '); + } +} + +export type GoogleSqlTimestampStringBuilderInitial = GoogleSqlTimestampStringBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlTimestampString'; + data: string; + driverParam: string | number; + enumValues: undefined; +}>; + +export class GoogleSqlTimestampStringBuilder> + extends GoogleSqlDateColumnBaseBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlTimestampStringBuilder'; + + constructor(name: T['name'], config: GoogleSqlTimestampConfig | undefined) { + super(name, 'string', 'GoogleSqlTimestampString'); + this.config.fsp = config?.fsp; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlTimestampString> { + return new GoogleSqlTimestampString>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlTimestampString> + extends GoogleSqlDateBaseColumn +{ + static override readonly [entityKind]: string = 'GoogleSqlTimestampString'; + + readonly fsp: number | undefined = this.config.fsp; + + getSQLType(): string { + const precision = this.fsp === undefined ? '' : `(${this.fsp})`; + return `timestamp${precision}`; + } +} + +export type TimestampFsp = 0 | 1 | 2 | 3 | 4 | 5 | 6; + +export interface GoogleSqlTimestampConfig { + mode?: TMode; + fsp?: TimestampFsp; +} + +export function timestamp(): GoogleSqlTimestampBuilderInitial<''>; +export function timestamp( + config?: GoogleSqlTimestampConfig, +): Equal extends true ? GoogleSqlTimestampStringBuilderInitial<''> + : GoogleSqlTimestampBuilderInitial<''>; +export function timestamp( + name: TName, + config?: GoogleSqlTimestampConfig, +): Equal extends true ? GoogleSqlTimestampStringBuilderInitial + : GoogleSqlTimestampBuilderInitial; +export function timestamp(a?: string | GoogleSqlTimestampConfig, b: GoogleSqlTimestampConfig = {}) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'string') { + return new GoogleSqlTimestampStringBuilder(name, config); + } + return new GoogleSqlTimestampBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/tinyint.ts b/drizzle-orm/src/googlesql/columns/tinyint.ts new file mode 100644 index 0000000000..471b5bf842 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/tinyint.ts @@ -0,0 +1,67 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumnBuilderWithAutoIncrement, GoogleSqlColumnWithAutoIncrement } from './common.ts'; +import type { GoogleSqlIntConfig } from './int.ts'; + +export type GoogleSqlTinyIntBuilderInitial = GoogleSqlTinyIntBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlTinyInt'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class GoogleSqlTinyIntBuilder> + extends GoogleSqlColumnBuilderWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlTinyIntBuilder'; + + constructor(name: T['name'], config?: GoogleSqlIntConfig) { + super(name, 'number', 'GoogleSqlTinyInt'); + this.config.unsigned = config ? config.unsigned : false; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlTinyInt> { + return new GoogleSqlTinyInt>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlTinyInt> + extends GoogleSqlColumnWithAutoIncrement +{ + static override readonly [entityKind]: string = 'GoogleSqlTinyInt'; + + getSQLType(): string { + return `tinyint${this.config.unsigned ? ' unsigned' : ''}`; + } + + override mapFromDriverValue(value: number | string): number { + if (typeof value === 'string') { + return Number(value); + } + return value; + } +} + +export function tinyint(): GoogleSqlTinyIntBuilderInitial<''>; +export function tinyint( + config?: GoogleSqlIntConfig, +): GoogleSqlTinyIntBuilderInitial<''>; +export function tinyint( + name: TName, + config?: GoogleSqlIntConfig, +): GoogleSqlTinyIntBuilderInitial; +export function tinyint(a?: string | GoogleSqlIntConfig, b?: GoogleSqlIntConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlTinyIntBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/varbinary.ts b/drizzle-orm/src/googlesql/columns/varbinary.ts new file mode 100644 index 0000000000..03533eb6d8 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/varbinary.ts @@ -0,0 +1,65 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlVarBinaryBuilderInitial = GoogleSqlVarBinaryBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlVarBinary'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class GoogleSqlVarBinaryBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlVarBinaryBuilder'; + + /** @internal */ + constructor(name: T['name'], config: GoogleSqlVarbinaryOptions) { + super(name, 'string', 'GoogleSqlVarBinary'); + this.config.length = config?.length; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlVarBinary> { + return new GoogleSqlVarBinary>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlVarBinary< + T extends ColumnBaseConfig<'string', 'GoogleSqlVarBinary'>, +> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlVarBinary'; + + length: number | undefined = this.config.length; + + getSQLType(): string { + return this.length === undefined ? `varbinary` : `varbinary(${this.length})`; + } +} + +export interface GoogleSqlVarbinaryOptions { + length: number; +} + +export function varbinary( + config: GoogleSqlVarbinaryOptions, +): GoogleSqlVarBinaryBuilderInitial<''>; +export function varbinary( + name: TName, + config: GoogleSqlVarbinaryOptions, +): GoogleSqlVarBinaryBuilderInitial; +export function varbinary(a?: string | GoogleSqlVarbinaryOptions, b?: GoogleSqlVarbinaryOptions) { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlVarBinaryBuilder(name, config); +} diff --git a/drizzle-orm/src/googlesql/columns/varchar.ts b/drizzle-orm/src/googlesql/columns/varchar.ts new file mode 100644 index 0000000000..9fd9b0a313 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/varchar.ts @@ -0,0 +1,84 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { getColumnNameAndConfig, type Writable } from '~/utils.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlVarCharBuilderInitial< + TName extends string, + TEnum extends [string, ...string[]], + TLength extends number | undefined, +> = GoogleSqlVarCharBuilder< + { + name: TName; + dataType: 'string'; + columnType: 'GoogleSqlVarChar'; + data: TEnum[number]; + driverParam: number | string; + enumValues: TEnum; + length: TLength; + } +>; + +export class GoogleSqlVarCharBuilder< + T extends ColumnBuilderBaseConfig<'string', 'GoogleSqlVarChar'> & { length?: number | undefined }, +> extends GoogleSqlColumnBuilder> { + static override readonly [entityKind]: string = 'GoogleSqlVarCharBuilder'; + + /** @internal */ + constructor(name: T['name'], config: GoogleSqlVarCharConfig) { + super(name, 'string', 'GoogleSqlVarChar'); + this.config.length = config.length; + this.config.enum = config.enum; + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlVarChar & { length: T['length']; enumValues: T['enumValues'] }> { + return new GoogleSqlVarChar & { length: T['length']; enumValues: T['enumValues'] }>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlVarChar< + T extends ColumnBaseConfig<'string', 'GoogleSqlVarChar'> & { length?: number | undefined }, +> extends GoogleSqlColumn, { length: T['length'] }> { + static override readonly [entityKind]: string = 'GoogleSqlVarChar'; + + readonly length: number | undefined = this.config.length; + + override readonly enumValues = this.config.enum; + + getSQLType(): string { + return this.length === undefined ? `varchar` : `varchar(${this.length})`; + } +} + +export interface GoogleSqlVarCharConfig< + TEnum extends string[] | readonly string[] | undefined = string[] | readonly string[] | undefined, + TLength extends number | undefined = number | undefined, +> { + enum?: TEnum; + length?: TLength; +} + +export function varchar, L extends number | undefined>( + config: GoogleSqlVarCharConfig, L>, +): GoogleSqlVarCharBuilderInitial<'', Writable, L>; +export function varchar< + TName extends string, + U extends string, + T extends Readonly<[U, ...U[]]>, + L extends number | undefined, +>( + name: TName, + config: GoogleSqlVarCharConfig, L>, +): GoogleSqlVarCharBuilderInitial, L>; +export function varchar(a?: string | GoogleSqlVarCharConfig, b?: GoogleSqlVarCharConfig): any { + const { name, config } = getColumnNameAndConfig(a, b); + return new GoogleSqlVarCharBuilder(name, config as any); +} diff --git a/drizzle-orm/src/googlesql/columns/year.ts b/drizzle-orm/src/googlesql/columns/year.ts new file mode 100644 index 0000000000..83d7947ba5 --- /dev/null +++ b/drizzle-orm/src/googlesql/columns/year.ts @@ -0,0 +1,50 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlTable } from '~/googlesql/table.ts'; +import { GoogleSqlColumn, GoogleSqlColumnBuilder } from './common.ts'; + +export type GoogleSqlYearBuilderInitial = GoogleSqlYearBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'GoogleSqlYear'; + data: number; + driverParam: number; + enumValues: undefined; +}>; + +export class GoogleSqlYearBuilder> + extends GoogleSqlColumnBuilder +{ + static override readonly [entityKind]: string = 'GoogleSqlYearBuilder'; + + constructor(name: T['name']) { + super(name, 'number', 'GoogleSqlYear'); + } + + /** @internal */ + override build( + table: AnyGoogleSqlTable<{ name: TTableName }>, + ): GoogleSqlYear> { + return new GoogleSqlYear>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class GoogleSqlYear< + T extends ColumnBaseConfig<'number', 'GoogleSqlYear'>, +> extends GoogleSqlColumn { + static override readonly [entityKind]: string = 'GoogleSqlYear'; + + getSQLType(): string { + return `year`; + } +} + +export function year(): GoogleSqlYearBuilderInitial<''>; +export function year(name: TName): GoogleSqlYearBuilderInitial; +export function year(name?: string) { + return new GoogleSqlYearBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/googlesql/db.ts b/drizzle-orm/src/googlesql/db.ts new file mode 100644 index 0000000000..65a26a2cdc --- /dev/null +++ b/drizzle-orm/src/googlesql/db.ts @@ -0,0 +1,536 @@ +import type { ResultSetHeader } from 'mysql2/promise'; +import { entityKind } from '~/entity.ts'; +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { ExtractTablesWithRelations, RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; +import { type ColumnsSelection, type SQL, sql, type SQLWrapper } from '~/sql/sql.ts'; +import { WithSubquery } from '~/subquery.ts'; +import type { DrizzleTypeError } from '~/utils.ts'; +import type { GoogleSqlDialect } from './dialect.ts'; +import { GoogleSqlCountBuilder } from './query-builders/count.ts'; +import { + GoogleSqlDeleteBase, + GoogleSqlInsertBuilder, + GoogleSqlSelectBuilder, + GoogleSqlUpdateBuilder, + QueryBuilder, +} from './query-builders/index.ts'; +import { RelationalQueryBuilder } from './query-builders/query.ts'; +import type { SelectedFields } from './query-builders/select.types.ts'; +import type { + GoogleSqlQueryResultHKT, + GoogleSqlQueryResultKind, + GoogleSqlSession, + GoogleSqlTransaction, + GoogleSqlTransactionConfig, + Mode, + PreparedQueryHKTBase, +} from './session.ts'; +import type { WithBuilder } from './subquery.ts'; +import type { GoogleSqlTable } from './table.ts'; +import type { GoogleSqlViewBase } from './view-base.ts'; + +export class GoogleSqlDatabase< + TQueryResult extends GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TFullSchema extends Record = {}, + TSchema extends TablesRelationalConfig = ExtractTablesWithRelations, +> { + static readonly [entityKind]: string = 'GoogleSqlDatabase'; + + declare readonly _: { + readonly schema: TSchema | undefined; + readonly fullSchema: TFullSchema; + readonly tableNamesMap: Record; + }; + + query: TFullSchema extends Record + ? DrizzleTypeError<'Seems like the schema generic is missing - did you forget to add it to your DB type?'> + : { + [K in keyof TSchema]: RelationalQueryBuilder; + }; + + constructor( + /** @internal */ + readonly dialect: GoogleSqlDialect, + /** @internal */ + readonly session: GoogleSqlSession, + schema: RelationalSchemaConfig | undefined, + protected readonly mode: Mode, + ) { + this._ = schema + ? { + schema: schema.schema, + fullSchema: schema.fullSchema as TFullSchema, + tableNamesMap: schema.tableNamesMap, + } + : { + schema: undefined, + fullSchema: {} as TFullSchema, + tableNamesMap: {}, + }; + this.query = {} as typeof this['query']; + if (this._.schema) { + for (const [tableName, columns] of Object.entries(this._.schema)) { + (this.query as GoogleSqlDatabase>['query'])[tableName] = + new RelationalQueryBuilder( + schema!.fullSchema, + this._.schema, + this._.tableNamesMap, + schema!.fullSchema[tableName] as GoogleSqlTable, + columns, + dialect, + session, + this.mode, + ); + } + } + } + + /** + * Creates a subquery that defines a temporary named result set as a CTE. + * + * It is useful for breaking down complex queries into simpler parts and for reusing the result set in subsequent parts of the query. + * + * See docs: {@link https://orm.drizzle.team/docs/select#with-clause} + * + * @param alias The alias for the subquery. + * + * Failure to provide an alias will result in a DrizzleTypeError, preventing the subquery from being referenced in other queries. + * + * @example + * + * ```ts + * // Create a subquery with alias 'sq' and use it in the select query + * const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42))); + * + * const result = await db.with(sq).select().from(sq); + * ``` + * + * To select arbitrary SQL values as fields in a CTE and reference them in other CTEs or in the main query, you need to add aliases to them: + * + * ```ts + * // Select an arbitrary SQL value as a field in a CTE and reference it in the main query + * const sq = db.$with('sq').as(db.select({ + * name: sql`upper(${users.name})`.as('name'), + * }) + * .from(users)); + * + * const result = await db.with(sq).select({ name: sq.name }).from(sq); + * ``` + */ + $with: WithBuilder = (alias: string, selection?: ColumnsSelection) => { + const self = this; + const as = ( + qb: + | TypedQueryBuilder + | SQL + | ((qb: QueryBuilder) => TypedQueryBuilder | SQL), + ) => { + if (typeof qb === 'function') { + qb = qb(new QueryBuilder(self.dialect)); + } + + return new Proxy( + new WithSubquery( + qb.getSQL(), + selection ?? ('getSelectedFields' in qb ? qb.getSelectedFields() ?? {} : {}) as SelectedFields, + alias, + true, + ), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), + ); + }; + return { as }; + }; + + $count( + source: GoogleSqlTable | GoogleSqlViewBase | SQL | SQLWrapper, + filters?: SQL, + ) { + return new GoogleSqlCountBuilder({ source, filters, session: this.session }); + } + + /** + * Incorporates a previously defined CTE (using `$with`) into the main query. + * + * This method allows the main query to reference a temporary named result set. + * + * See docs: {@link https://orm.drizzle.team/docs/select#with-clause} + * + * @param queries The CTEs to incorporate into the main query. + * + * @example + * + * ```ts + * // Define a subquery 'sq' as a CTE using $with + * const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42))); + * + * // Incorporate the CTE 'sq' into the main query and select from it + * const result = await db.with(sq).select().from(sq); + * ``` + */ + with(...queries: WithSubquery[]) { + const self = this; + + /** + * Creates a select query. + * + * Calling this method with no arguments will select all columns from the table. Pass a selection object to specify the columns you want to select. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select} + * + * @param fields The selection object. + * + * @example + * + * ```ts + * // Select all columns and all rows from the 'cars' table + * const allCars: Car[] = await db.select().from(cars); + * + * // Select specific columns and all rows from the 'cars' table + * const carsIdsAndBrands: { id: number; brand: string }[] = await db.select({ + * id: cars.id, + * brand: cars.brand + * }) + * .from(cars); + * ``` + * + * Like in SQL, you can use arbitrary expressions as selection fields, not just table columns: + * + * ```ts + * // Select specific columns along with expression and all rows from the 'cars' table + * const carsIdsAndLowerNames: { id: number; lowerBrand: string }[] = await db.select({ + * id: cars.id, + * lowerBrand: sql`lower(${cars.brand})`, + * }) + * .from(cars); + * ``` + */ + function select(): GoogleSqlSelectBuilder; + function select( + fields: TSelection, + ): GoogleSqlSelectBuilder; + function select(fields?: SelectedFields): GoogleSqlSelectBuilder { + return new GoogleSqlSelectBuilder({ + fields: fields ?? undefined, + session: self.session, + dialect: self.dialect, + withList: queries, + }); + } + + /** + * Adds `distinct` expression to the select query. + * + * Calling this method will return only unique values. When multiple columns are selected, it returns rows with unique combinations of values in these columns. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select#distinct} + * + * @param fields The selection object. + * + * @example + * ```ts + * // Select all unique rows from the 'cars' table + * await db.selectDistinct() + * .from(cars) + * .orderBy(cars.id, cars.brand, cars.color); + * + * // Select all unique brands from the 'cars' table + * await db.selectDistinct({ brand: cars.brand }) + * .from(cars) + * .orderBy(cars.brand); + * ``` + */ + function selectDistinct(): GoogleSqlSelectBuilder; + function selectDistinct( + fields: TSelection, + ): GoogleSqlSelectBuilder; + function selectDistinct( + fields?: SelectedFields, + ): GoogleSqlSelectBuilder { + return new GoogleSqlSelectBuilder({ + fields: fields ?? undefined, + session: self.session, + dialect: self.dialect, + withList: queries, + distinct: true, + }); + } + + /** + * Creates an update query. + * + * Calling this method without `.where()` clause will update all rows in a table. The `.where()` clause specifies which rows should be updated. + * + * Use `.set()` method to specify which values to update. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param table The table to update. + * + * @example + * + * ```ts + * // Update all rows in the 'cars' table + * await db.update(cars).set({ color: 'red' }); + * + * // Update rows with filters and conditions + * await db.update(cars).set({ color: 'red' }).where(eq(cars.brand, 'BMW')); + * ``` + */ + function update( + table: TTable, + ): GoogleSqlUpdateBuilder { + return new GoogleSqlUpdateBuilder(table, self.session, self.dialect, queries); + } + + /** + * Creates a delete query. + * + * Calling this method without `.where()` clause will delete all rows in a table. The `.where()` clause specifies which rows should be deleted. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param table The table to delete from. + * + * @example + * + * ```ts + * // Delete all rows in the 'cars' table + * await db.delete(cars); + * + * // Delete rows with filters and conditions + * await db.delete(cars).where(eq(cars.color, 'green')); + * ``` + */ + function delete_( + table: TTable, + ): GoogleSqlDeleteBase { + return new GoogleSqlDeleteBase(table, self.session, self.dialect, queries); + } + + return { select, selectDistinct, update, delete: delete_ }; + } + + /** + * Creates a select query. + * + * Calling this method with no arguments will select all columns from the table. Pass a selection object to specify the columns you want to select. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select} + * + * @param fields The selection object. + * + * @example + * + * ```ts + * // Select all columns and all rows from the 'cars' table + * const allCars: Car[] = await db.select().from(cars); + * + * // Select specific columns and all rows from the 'cars' table + * const carsIdsAndBrands: { id: number; brand: string }[] = await db.select({ + * id: cars.id, + * brand: cars.brand + * }) + * .from(cars); + * ``` + * + * Like in SQL, you can use arbitrary expressions as selection fields, not just table columns: + * + * ```ts + * // Select specific columns along with expression and all rows from the 'cars' table + * const carsIdsAndLowerNames: { id: number; lowerBrand: string }[] = await db.select({ + * id: cars.id, + * lowerBrand: sql`lower(${cars.brand})`, + * }) + * .from(cars); + * ``` + */ + select(): GoogleSqlSelectBuilder; + select(fields: TSelection): GoogleSqlSelectBuilder; + select(fields?: SelectedFields): GoogleSqlSelectBuilder { + return new GoogleSqlSelectBuilder({ fields: fields ?? undefined, session: this.session, dialect: this.dialect }); + } + + /** + * Adds `distinct` expression to the select query. + * + * Calling this method will return only unique values. When multiple columns are selected, it returns rows with unique combinations of values in these columns. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select#distinct} + * + * @param fields The selection object. + * + * @example + * ```ts + * // Select all unique rows from the 'cars' table + * await db.selectDistinct() + * .from(cars) + * .orderBy(cars.id, cars.brand, cars.color); + * + * // Select all unique brands from the 'cars' table + * await db.selectDistinct({ brand: cars.brand }) + * .from(cars) + * .orderBy(cars.brand); + * ``` + */ + selectDistinct(): GoogleSqlSelectBuilder; + selectDistinct( + fields: TSelection, + ): GoogleSqlSelectBuilder; + selectDistinct(fields?: SelectedFields): GoogleSqlSelectBuilder { + return new GoogleSqlSelectBuilder({ + fields: fields ?? undefined, + session: this.session, + dialect: this.dialect, + distinct: true, + }); + } + + /** + * Creates an update query. + * + * Calling this method without `.where()` clause will update all rows in a table. The `.where()` clause specifies which rows should be updated. + * + * Use `.set()` method to specify which values to update. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param table The table to update. + * + * @example + * + * ```ts + * // Update all rows in the 'cars' table + * await db.update(cars).set({ color: 'red' }); + * + * // Update rows with filters and conditions + * await db.update(cars).set({ color: 'red' }).where(eq(cars.brand, 'BMW')); + * ``` + */ + update( + table: TTable, + ): GoogleSqlUpdateBuilder { + return new GoogleSqlUpdateBuilder(table, this.session, this.dialect); + } + + /** + * Creates an insert query. + * + * Calling this method will create new rows in a table. Use `.values()` method to specify which values to insert. + * + * See docs: {@link https://orm.drizzle.team/docs/insert} + * + * @param table The table to insert into. + * + * @example + * + * ```ts + * // Insert one row + * await db.insert(cars).values({ brand: 'BMW' }); + * + * // Insert multiple rows + * await db.insert(cars).values([{ brand: 'BMW' }, { brand: 'Porsche' }]); + * ``` + */ + insert( + table: TTable, + ): GoogleSqlInsertBuilder { + return new GoogleSqlInsertBuilder(table, this.session, this.dialect); + } + + /** + * Creates a delete query. + * + * Calling this method without `.where()` clause will delete all rows in a table. The `.where()` clause specifies which rows should be deleted. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param table The table to delete from. + * + * @example + * + * ```ts + * // Delete all rows in the 'cars' table + * await db.delete(cars); + * + * // Delete rows with filters and conditions + * await db.delete(cars).where(eq(cars.color, 'green')); + * ``` + */ + delete(table: TTable): GoogleSqlDeleteBase { + return new GoogleSqlDeleteBase(table, this.session, this.dialect); + } + + execute( + query: SQLWrapper | string, + ): Promise> { + return this.session.execute(typeof query === 'string' ? sql.raw(query) : query.getSQL()); + } + + transaction( + transaction: ( + tx: GoogleSqlTransaction, + config?: GoogleSqlTransactionConfig, + ) => Promise, + config?: GoogleSqlTransactionConfig, + ): Promise { + return this.session.transaction(transaction, config); + } +} + +export type MySQLWithReplicas = Q & { $primary: Q }; + +export const withReplicas = < + HKT extends GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TFullSchema extends Record, + TSchema extends TablesRelationalConfig, + Q extends GoogleSqlDatabase< + HKT, + TPreparedQueryHKT, + TFullSchema, + TSchema extends Record ? ExtractTablesWithRelations : TSchema + >, +>( + primary: Q, + replicas: [Q, ...Q[]], + getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!, +): MySQLWithReplicas => { + const select: Q['select'] = (...args: []) => getReplica(replicas).select(...args); + const selectDistinct: Q['selectDistinct'] = (...args: []) => getReplica(replicas).selectDistinct(...args); + const $count: Q['$count'] = (...args: [any]) => getReplica(replicas).$count(...args); + const $with: Q['with'] = (...args: []) => getReplica(replicas).with(...args); + + const update: Q['update'] = (...args: [any]) => primary.update(...args); + const insert: Q['insert'] = (...args: [any]) => primary.insert(...args); + const $delete: Q['delete'] = (...args: [any]) => primary.delete(...args); + const execute: Q['execute'] = (...args: [any]) => primary.execute(...args); + const transaction: Q['transaction'] = (...args: [any, any]) => primary.transaction(...args); + + return { + ...primary, + update, + insert, + delete: $delete, + execute, + transaction, + $primary: primary, + select, + selectDistinct, + $count, + with: $with, + get query() { + return getReplica(replicas).query; + }, + }; +}; diff --git a/drizzle-orm/src/googlesql/dialect.ts b/drizzle-orm/src/googlesql/dialect.ts new file mode 100644 index 0000000000..bb046b7dc2 --- /dev/null +++ b/drizzle-orm/src/googlesql/dialect.ts @@ -0,0 +1,1148 @@ +import { aliasedTable, aliasedTableColumn, mapColumnsInAliasedSQLToAlias, mapColumnsInSQLToAlias } from '~/alias.ts'; +import { CasingCache } from '~/casing.ts'; +import { Column } from '~/column.ts'; +import { entityKind, is } from '~/entity.ts'; +import { DrizzleError } from '~/errors.ts'; +import { and, eq } from '~/expressions.ts'; +import type { MigrationConfig, MigrationMeta } from '~/migrator.ts'; +import { + type BuildRelationalQueryResult, + type DBQueryConfig, + getOperators, + getOrderByOperators, + Many, + normalizeRelation, + One, + type Relation, + type TableRelationalConfig, + type TablesRelationalConfig, +} from '~/relations.ts'; +import { Param, SQL, sql, View } from '~/sql/sql.ts'; +import type { Name, Placeholder, QueryWithTypings, SQLChunk } from '~/sql/sql.ts'; +import { Subquery } from '~/subquery.ts'; +import { getTableName, getTableUniqueName, Table } from '~/table.ts'; +import { type Casing, orderSelectedFields, type UpdateSet } from '~/utils.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import { GoogleSqlColumn } from './columns/common.ts'; +import type { GoogleSqlDeleteConfig } from './query-builders/delete.ts'; +import type { GoogleSqlInsertConfig } from './query-builders/insert.ts'; +import type { + AnyGoogleSqlSelectQueryBuilder, + GoogleSqlSelectConfig, + GoogleSqlSelectJoinConfig, + SelectedFieldsOrdered, +} from './query-builders/select.types.ts'; +import type { GoogleSqlUpdateConfig } from './query-builders/update.ts'; +import type { GoogleSqlSession } from './session.ts'; +import { GoogleSqlTable } from './table.ts'; +import { GoogleSqlViewBase } from './view-base.ts'; + +export interface GoogleSqlDialectConfig { + casing?: Casing; +} + +export class GoogleSqlDialect { + static readonly [entityKind]: string = 'GoogleSqlDialect'; + + /** @internal */ + readonly casing: CasingCache; + + constructor(config?: GoogleSqlDialectConfig) { + this.casing = new CasingCache(config?.casing); + } + + async migrate( + migrations: MigrationMeta[], + session: GoogleSqlSession, + config: Omit, + ): Promise { + const migrationsTable = config.migrationsTable ?? '__drizzle_migrations'; + const migrationTableCreate = sql` + create table if not exists ${sql.identifier(migrationsTable)} ( + id serial primary key, + hash text not null, + created_at bigint + ) + `; + await session.execute(migrationTableCreate); + + const dbMigrations = await session.all<{ id: number; hash: string; created_at: string }>( + sql`select id, hash, created_at from ${sql.identifier(migrationsTable)} order by created_at desc limit 1`, + ); + + const lastDbMigration = dbMigrations[0]; + + await session.transaction(async (tx) => { + for (const migration of migrations) { + if ( + !lastDbMigration + || Number(lastDbMigration.created_at) < migration.folderMillis + ) { + for (const stmt of migration.sql) { + await tx.execute(sql.raw(stmt)); + } + await tx.execute( + sql`insert into ${ + sql.identifier(migrationsTable) + } (\`hash\`, \`created_at\`) values(${migration.hash}, ${migration.folderMillis})`, + ); + } + } + }); + } + + escapeName(name: string): string { + return `\`${name}\``; + } + + escapeParam(_num: number): string { + return `?`; + } + + escapeString(str: string): string { + return `'${str.replace(/'/g, "''")}'`; + } + + private buildWithCTE(queries: Subquery[] | undefined): SQL | undefined { + if (!queries?.length) return undefined; + + const withSqlChunks = [sql`with `]; + for (const [i, w] of queries.entries()) { + withSqlChunks.push(sql`${sql.identifier(w._.alias)} as (${w._.sql})`); + if (i < queries.length - 1) { + withSqlChunks.push(sql`, `); + } + } + withSqlChunks.push(sql` `); + return sql.join(withSqlChunks); + } + + buildDeleteQuery({ table, where, returning, withList, limit, orderBy }: GoogleSqlDeleteConfig): SQL { + const withSql = this.buildWithCTE(withList); + + const returningSql = returning + ? sql` returning ${this.buildSelection(returning, { isSingleTable: true })}` + : undefined; + + const whereSql = where ? sql` where ${where}` : undefined; + + const orderBySql = this.buildOrderBy(orderBy); + + const limitSql = this.buildLimit(limit); + + return sql`${withSql}delete from ${table}${whereSql}${orderBySql}${limitSql}${returningSql}`; + } + + buildUpdateSet(table: GoogleSqlTable, set: UpdateSet): SQL { + const tableColumns = table[Table.Symbol.Columns]; + + const columnNames = Object.keys(tableColumns).filter((colName) => + set[colName] !== undefined || tableColumns[colName]?.onUpdateFn !== undefined + ); + + const setSize = columnNames.length; + return sql.join(columnNames.flatMap((colName, i) => { + const col = tableColumns[colName]!; + + const value = set[colName] ?? sql.param(col.onUpdateFn!(), col); + const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`; + + if (i < setSize - 1) { + return [res, sql.raw(', ')]; + } + return [res]; + })); + } + + buildUpdateQuery({ table, set, where, returning, withList, limit, orderBy }: GoogleSqlUpdateConfig): SQL { + const withSql = this.buildWithCTE(withList); + + const setSql = this.buildUpdateSet(table, set); + + const returningSql = returning + ? sql` returning ${this.buildSelection(returning, { isSingleTable: true })}` + : undefined; + + const whereSql = where ? sql` where ${where}` : undefined; + + const orderBySql = this.buildOrderBy(orderBy); + + const limitSql = this.buildLimit(limit); + + return sql`${withSql}update ${table} set ${setSql}${whereSql}${orderBySql}${limitSql}${returningSql}`; + } + + /** + * Builds selection SQL with provided fields/expressions + * + * Examples: + * + * `select from` + * + * `insert ... returning ` + * + * If `isSingleTable` is true, then columns won't be prefixed with table name + */ + private buildSelection( + fields: SelectedFieldsOrdered, + { isSingleTable = false }: { isSingleTable?: boolean } = {}, + ): SQL { + const columnsLen = fields.length; + + const chunks = fields + .flatMap(({ field }, i) => { + const chunk: SQLChunk[] = []; + + if (is(field, SQL.Aliased) && field.isSelectionField) { + chunk.push(sql.identifier(field.fieldAlias)); + } else if (is(field, SQL.Aliased) || is(field, SQL)) { + const query = is(field, SQL.Aliased) ? field.sql : field; + + if (isSingleTable) { + chunk.push( + new SQL( + query.queryChunks.map((c) => { + if (is(c, GoogleSqlColumn)) { + return sql.identifier(this.casing.getColumnCasing(c)); + } + return c; + }), + ), + ); + } else { + chunk.push(query); + } + + if (is(field, SQL.Aliased)) { + chunk.push(sql` as ${sql.identifier(field.fieldAlias)}`); + } + } else if (is(field, Column)) { + if (isSingleTable) { + chunk.push(sql.identifier(this.casing.getColumnCasing(field))); + } else { + chunk.push(field); + } + } + + if (i < columnsLen - 1) { + chunk.push(sql`, `); + } + + return chunk; + }); + + return sql.join(chunks); + } + + private buildLimit(limit: number | Placeholder | undefined): SQL | undefined { + return typeof limit === 'object' || (typeof limit === 'number' && limit >= 0) + ? sql` limit ${limit}` + : undefined; + } + + private buildOrderBy(orderBy: (GoogleSqlColumn | SQL | SQL.Aliased)[] | undefined): SQL | undefined { + return orderBy && orderBy.length > 0 ? sql` order by ${sql.join(orderBy, sql`, `)}` : undefined; + } + + private buildIndex({ + indexes, + indexFor, + }: { + indexes: string[] | undefined; + indexFor: 'USE' | 'FORCE' | 'IGNORE'; + }): SQL | undefined { + return indexes && indexes.length > 0 + ? sql` ${sql.raw(indexFor)} INDEX (${sql.raw(indexes.join(`, `))})` + : undefined; + } + + buildSelectQuery( + { + withList, + fields, + fieldsFlat, + where, + having, + table, + joins, + orderBy, + groupBy, + limit, + offset, + lockingClause, + distinct, + setOperators, + useIndex, + forceIndex, + ignoreIndex, + }: GoogleSqlSelectConfig, + ): SQL { + const fieldsList = fieldsFlat ?? orderSelectedFields(fields); + for (const f of fieldsList) { + if ( + is(f.field, Column) + && getTableName(f.field.table) + !== (is(table, Subquery) + ? table._.alias + : is(table, GoogleSqlViewBase) + ? table[ViewBaseConfig].name + : is(table, SQL) + ? undefined + : getTableName(table)) + && !((table) => + joins?.some(({ alias }) => + alias === (table[Table.Symbol.IsAlias] ? getTableName(table) : table[Table.Symbol.BaseName]) + ))(f.field.table) + ) { + const tableName = getTableName(f.field.table); + throw new Error( + `Your "${ + f.path.join('->') + }" field references a column "${tableName}"."${f.field.name}", but the table "${tableName}" is not part of the query! Did you forget to join it?`, + ); + } + } + + const isSingleTable = !joins || joins.length === 0; + + const withSql = this.buildWithCTE(withList); + + const distinctSql = distinct ? sql` distinct` : undefined; + + const selection = this.buildSelection(fieldsList, { isSingleTable }); + + const tableSql = (() => { + if (is(table, Table) && table[Table.Symbol.OriginalName] !== table[Table.Symbol.Name]) { + return sql`${sql.identifier(table[Table.Symbol.OriginalName])} ${sql.identifier(table[Table.Symbol.Name])}`; + } + + return table; + })(); + + const joinsArray: SQL[] = []; + + if (joins) { + for (const [index, joinMeta] of joins.entries()) { + if (index === 0) { + joinsArray.push(sql` `); + } + const table = joinMeta.table; + const lateralSql = joinMeta.lateral ? sql` lateral` : undefined; + + if (is(table, GoogleSqlTable)) { + const tableName = table[GoogleSqlTable.Symbol.Name]; + const tableSchema = table[GoogleSqlTable.Symbol.Schema]; + const origTableName = table[GoogleSqlTable.Symbol.OriginalName]; + const alias = tableName === origTableName ? undefined : joinMeta.alias; + const useIndexSql = this.buildIndex({ indexes: joinMeta.useIndex, indexFor: 'USE' }); + const forceIndexSql = this.buildIndex({ indexes: joinMeta.forceIndex, indexFor: 'FORCE' }); + const ignoreIndexSql = this.buildIndex({ indexes: joinMeta.ignoreIndex, indexFor: 'IGNORE' }); + joinsArray.push( + sql`${sql.raw(joinMeta.joinType)} join${lateralSql} ${ + tableSchema ? sql`${sql.identifier(tableSchema)}.` : undefined + }${sql.identifier(origTableName)}${useIndexSql}${forceIndexSql}${ignoreIndexSql}${ + alias && sql` ${sql.identifier(alias)}` + } on ${joinMeta.on}`, + ); + } else if (is(table, View)) { + const viewName = table[ViewBaseConfig].name; + const viewSchema = table[ViewBaseConfig].schema; + const origViewName = table[ViewBaseConfig].originalName; + const alias = viewName === origViewName ? undefined : joinMeta.alias; + joinsArray.push( + sql`${sql.raw(joinMeta.joinType)} join${lateralSql} ${ + viewSchema ? sql`${sql.identifier(viewSchema)}.` : undefined + }${sql.identifier(origViewName)}${alias && sql` ${sql.identifier(alias)}`} on ${joinMeta.on}`, + ); + } else { + joinsArray.push( + sql`${sql.raw(joinMeta.joinType)} join${lateralSql} ${table} on ${joinMeta.on}`, + ); + } + if (index < joins.length - 1) { + joinsArray.push(sql` `); + } + } + } + + const joinsSql = sql.join(joinsArray); + + const whereSql = where ? sql` where ${where}` : undefined; + + const havingSql = having ? sql` having ${having}` : undefined; + + const orderBySql = this.buildOrderBy(orderBy); + + const groupBySql = groupBy && groupBy.length > 0 ? sql` group by ${sql.join(groupBy, sql`, `)}` : undefined; + + const limitSql = this.buildLimit(limit); + + const offsetSql = offset ? sql` offset ${offset}` : undefined; + + const useIndexSql = this.buildIndex({ indexes: useIndex, indexFor: 'USE' }); + + const forceIndexSql = this.buildIndex({ indexes: forceIndex, indexFor: 'FORCE' }); + + const ignoreIndexSql = this.buildIndex({ indexes: ignoreIndex, indexFor: 'IGNORE' }); + + let lockingClausesSql; + if (lockingClause) { + const { config, strength } = lockingClause; + lockingClausesSql = sql` for ${sql.raw(strength)}`; + if (config.noWait) { + lockingClausesSql.append(sql` no wait`); + } else if (config.skipLocked) { + lockingClausesSql.append(sql` skip locked`); + } + } + + const finalQuery = + sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${useIndexSql}${forceIndexSql}${ignoreIndexSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}${lockingClausesSql}`; + + if (setOperators.length > 0) { + return this.buildSetOperations(finalQuery, setOperators); + } + + return finalQuery; + } + + buildSetOperations(leftSelect: SQL, setOperators: GoogleSqlSelectConfig['setOperators']): SQL { + const [setOperator, ...rest] = setOperators; + + if (!setOperator) { + throw new Error('Cannot pass undefined values to any set operator'); + } + + if (rest.length === 0) { + return this.buildSetOperationQuery({ leftSelect, setOperator }); + } + + // Some recursive magic here + return this.buildSetOperations( + this.buildSetOperationQuery({ leftSelect, setOperator }), + rest, + ); + } + + buildSetOperationQuery({ + leftSelect, + setOperator: { type, isAll, rightSelect, limit, orderBy, offset }, + }: { leftSelect: SQL; setOperator: GoogleSqlSelectConfig['setOperators'][number] }): SQL { + const leftChunk = sql`(${leftSelect.getSQL()}) `; + const rightChunk = sql`(${rightSelect.getSQL()})`; + + let orderBySql; + if (orderBy && orderBy.length > 0) { + const orderByValues: (SQL | Name)[] = []; + + // The next bit is necessary because the sql operator replaces ${table.column} with `table`.`column` + // which is invalid GoogleSql syntax, Table from one of the SELECTs cannot be used in global ORDER clause + for (const orderByUnit of orderBy) { + if (is(orderByUnit, GoogleSqlColumn)) { + orderByValues.push(sql.identifier(this.casing.getColumnCasing(orderByUnit))); + } else if (is(orderByUnit, SQL)) { + for (let i = 0; i < orderByUnit.queryChunks.length; i++) { + const chunk = orderByUnit.queryChunks[i]; + + if (is(chunk, GoogleSqlColumn)) { + orderByUnit.queryChunks[i] = sql.identifier(this.casing.getColumnCasing(chunk)); + } + } + + orderByValues.push(sql`${orderByUnit}`); + } else { + orderByValues.push(sql`${orderByUnit}`); + } + } + + orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)} `; + } + + const limitSql = typeof limit === 'object' || (typeof limit === 'number' && limit >= 0) + ? sql` limit ${limit}` + : undefined; + + const operatorChunk = sql.raw(`${type} ${isAll ? 'all ' : ''}`); + + const offsetSql = offset ? sql` offset ${offset}` : undefined; + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; + } + + buildInsertQuery( + { table, values: valuesOrSelect, ignore, onConflict, select }: GoogleSqlInsertConfig, + ): { sql: SQL; generatedIds: Record[] } { + // const isSingleValue = values.length === 1; + const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; + const columns: Record = table[Table.Symbol.Columns]; + const colEntries: [string, GoogleSqlColumn][] = Object.entries(columns).filter(([_, col]) => + !col.shouldDisableInsert() + ); + + const insertOrder = colEntries.map(([, column]) => sql.identifier(this.casing.getColumnCasing(column))); + const generatedIdsResponse: Record[] = []; + + if (select) { + const select = valuesOrSelect as AnyGoogleSqlSelectQueryBuilder | SQL; + + if (is(select, SQL)) { + valuesSqlList.push(select); + } else { + valuesSqlList.push(select.getSQL()); + } + } else { + const values = valuesOrSelect as Record[]; + valuesSqlList.push(sql.raw('values ')); + + for (const [valueIndex, value] of values.entries()) { + const generatedIds: Record = {}; + + const valueList: (SQLChunk | SQL)[] = []; + for (const [fieldName, col] of colEntries) { + const colValue = value[fieldName]; + if (colValue === undefined || (is(colValue, Param) && colValue.value === undefined)) { + // eslint-disable-next-line unicorn/no-negated-condition + if (col.defaultFn !== undefined) { + const defaultFnResult = col.defaultFn(); + generatedIds[fieldName] = defaultFnResult; + const defaultValue = is(defaultFnResult, SQL) ? defaultFnResult : sql.param(defaultFnResult, col); + valueList.push(defaultValue); + // eslint-disable-next-line unicorn/no-negated-condition + } else if (!col.default && col.onUpdateFn !== undefined) { + const onUpdateFnResult = col.onUpdateFn(); + const newValue = is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col); + valueList.push(newValue); + } else { + valueList.push(sql`default`); + } + } else { + if (col.defaultFn && is(colValue, Param)) { + generatedIds[fieldName] = colValue.value; + } + valueList.push(colValue); + } + } + + generatedIdsResponse.push(generatedIds); + valuesSqlList.push(valueList); + if (valueIndex < values.length - 1) { + valuesSqlList.push(sql`, `); + } + } + } + + const valuesSql = sql.join(valuesSqlList); + + const ignoreSql = ignore ? sql` ignore` : undefined; + + const onConflictSql = onConflict ? sql` on duplicate key ${onConflict}` : undefined; + + return { + sql: sql`insert${ignoreSql} into ${table} ${insertOrder} ${valuesSql}${onConflictSql}`, + generatedIds: generatedIdsResponse, + }; + } + + sqlToQuery(sql: SQL, invokeSource?: 'indexes' | undefined): QueryWithTypings { + return sql.toQuery({ + casing: this.casing, + escapeName: this.escapeName, + escapeParam: this.escapeParam, + escapeString: this.escapeString, + invokeSource, + }); + } + + buildRelationalQuery({ + fullSchema, + schema, + tableNamesMap, + table, + tableConfig, + queryConfig: config, + tableAlias, + nestedQueryRelation, + joinOn, + }: { + fullSchema: Record; + schema: TablesRelationalConfig; + tableNamesMap: Record; + table: GoogleSqlTable; + tableConfig: TableRelationalConfig; + queryConfig: true | DBQueryConfig<'many', true>; + tableAlias: string; + nestedQueryRelation?: Relation; + joinOn?: SQL; + }): BuildRelationalQueryResult { + let selection: BuildRelationalQueryResult['selection'] = []; + let limit, offset, orderBy: GoogleSqlSelectConfig['orderBy'], where; + const joins: GoogleSqlSelectJoinConfig[] = []; + + if (config === true) { + const selectionEntries = Object.entries(tableConfig.columns); + selection = selectionEntries.map(( + [key, value], + ) => ({ + dbKey: value.name, + tsKey: key, + field: aliasedTableColumn(value as GoogleSqlColumn, tableAlias), + relationTableTsKey: undefined, + isJson: false, + selection: [], + })); + } else { + const aliasedColumns = Object.fromEntries( + Object.entries(tableConfig.columns).map(([key, value]) => [key, aliasedTableColumn(value, tableAlias)]), + ); + + if (config.where) { + const whereSql = typeof config.where === 'function' + ? config.where(aliasedColumns, getOperators()) + : config.where; + where = whereSql && mapColumnsInSQLToAlias(whereSql, tableAlias); + } + + const fieldsSelection: { tsKey: string; value: GoogleSqlColumn | SQL.Aliased }[] = []; + let selectedColumns: string[] = []; + + // Figure out which columns to select + if (config.columns) { + let isIncludeMode = false; + + for (const [field, value] of Object.entries(config.columns)) { + if (value === undefined) { + continue; + } + + if (field in tableConfig.columns) { + if (!isIncludeMode && value === true) { + isIncludeMode = true; + } + selectedColumns.push(field); + } + } + + if (selectedColumns.length > 0) { + selectedColumns = isIncludeMode + ? selectedColumns.filter((c) => config.columns?.[c] === true) + : Object.keys(tableConfig.columns).filter((key) => !selectedColumns.includes(key)); + } + } else { + // Select all columns if selection is not specified + selectedColumns = Object.keys(tableConfig.columns); + } + + for (const field of selectedColumns) { + const column = tableConfig.columns[field]! as GoogleSqlColumn; + fieldsSelection.push({ tsKey: field, value: column }); + } + + let selectedRelations: { + tsKey: string; + queryConfig: true | DBQueryConfig<'many', false>; + relation: Relation; + }[] = []; + + // Figure out which relations to select + if (config.with) { + selectedRelations = Object.entries(config.with) + .filter((entry): entry is [typeof entry[0], NonNullable] => !!entry[1]) + .map(([tsKey, queryConfig]) => ({ tsKey, queryConfig, relation: tableConfig.relations[tsKey]! })); + } + + let extras; + + // Figure out which extras to select + if (config.extras) { + extras = typeof config.extras === 'function' + ? config.extras(aliasedColumns, { sql }) + : config.extras; + for (const [tsKey, value] of Object.entries(extras)) { + fieldsSelection.push({ + tsKey, + value: mapColumnsInAliasedSQLToAlias(value, tableAlias), + }); + } + } + + // Transform `fieldsSelection` into `selection` + // `fieldsSelection` shouldn't be used after this point + for (const { tsKey, value } of fieldsSelection) { + selection.push({ + dbKey: is(value, SQL.Aliased) ? value.fieldAlias : tableConfig.columns[tsKey]!.name, + tsKey, + field: is(value, Column) ? aliasedTableColumn(value, tableAlias) : value, + relationTableTsKey: undefined, + isJson: false, + selection: [], + }); + } + + let orderByOrig = typeof config.orderBy === 'function' + ? config.orderBy(aliasedColumns, getOrderByOperators()) + : config.orderBy ?? []; + if (!Array.isArray(orderByOrig)) { + orderByOrig = [orderByOrig]; + } + orderBy = orderByOrig.map((orderByValue) => { + if (is(orderByValue, Column)) { + return aliasedTableColumn(orderByValue, tableAlias) as GoogleSqlColumn; + } + return mapColumnsInSQLToAlias(orderByValue, tableAlias); + }); + + limit = config.limit; + offset = config.offset; + + // Process all relations + for ( + const { + tsKey: selectedRelationTsKey, + queryConfig: selectedRelationConfigValue, + relation, + } of selectedRelations + ) { + const normalizedRelation = normalizeRelation(schema, tableNamesMap, relation); + const relationTableName = getTableUniqueName(relation.referencedTable); + const relationTableTsName = tableNamesMap[relationTableName]!; + const relationTableAlias = `${tableAlias}_${selectedRelationTsKey}`; + const joinOn = and( + ...normalizedRelation.fields.map((field, i) => + eq( + aliasedTableColumn(normalizedRelation.references[i]!, relationTableAlias), + aliasedTableColumn(field, tableAlias), + ) + ), + ); + const builtRelation = this.buildRelationalQuery({ + fullSchema, + schema, + tableNamesMap, + table: fullSchema[relationTableTsName] as GoogleSqlTable, + tableConfig: schema[relationTableTsName]!, + queryConfig: is(relation, One) + ? (selectedRelationConfigValue === true + ? { limit: 1 } + : { ...selectedRelationConfigValue, limit: 1 }) + : selectedRelationConfigValue, + tableAlias: relationTableAlias, + joinOn, + nestedQueryRelation: relation, + }); + const field = sql`${sql.identifier(relationTableAlias)}.${sql.identifier('data')}`.as(selectedRelationTsKey); + joins.push({ + on: sql`true`, + table: new Subquery(builtRelation.sql as SQL, {}, relationTableAlias), + alias: relationTableAlias, + joinType: 'left', + lateral: true, + }); + selection.push({ + dbKey: selectedRelationTsKey, + tsKey: selectedRelationTsKey, + field, + relationTableTsKey: relationTableTsName, + isJson: true, + selection: builtRelation.selection, + }); + } + } + + if (selection.length === 0) { + throw new DrizzleError({ message: `No fields selected for table "${tableConfig.tsName}" ("${tableAlias}")` }); + } + + let result; + + where = and(joinOn, where); + + if (nestedQueryRelation) { + let field = sql`json_array(${ + sql.join( + selection.map(({ field, tsKey, isJson }) => + isJson + ? sql`${sql.identifier(`${tableAlias}_${tsKey}`)}.${sql.identifier('data')}` + : is(field, SQL.Aliased) + ? field.sql + : field + ), + sql`, `, + ) + })`; + if (is(nestedQueryRelation, Many)) { + field = sql`coalesce(json_arrayagg(${field}), json_array())`; + } + const nestedSelection = [{ + dbKey: 'data', + tsKey: 'data', + field: field.as('data'), + isJson: true, + relationTableTsKey: tableConfig.tsName, + selection, + }]; + + const needsSubquery = limit !== undefined || offset !== undefined || (orderBy?.length ?? 0) > 0; + + if (needsSubquery) { + result = this.buildSelectQuery({ + table: aliasedTable(table, tableAlias), + fields: {}, + fieldsFlat: [ + { + path: [], + field: sql.raw('*'), + }, + ...(((orderBy?.length ?? 0) > 0) + ? [{ + path: [], + field: sql`row_number() over (order by ${sql.join(orderBy!, sql`, `)})`, + }] + : []), + ], + where, + limit, + offset, + setOperators: [], + }); + + where = undefined; + limit = undefined; + offset = undefined; + orderBy = undefined; + } else { + result = aliasedTable(table, tableAlias); + } + + result = this.buildSelectQuery({ + table: is(result, GoogleSqlTable) ? result : new Subquery(result, {}, tableAlias), + fields: {}, + fieldsFlat: nestedSelection.map(({ field }) => ({ + path: [], + field: is(field, Column) ? aliasedTableColumn(field, tableAlias) : field, + })), + joins, + where, + limit, + offset, + orderBy, + setOperators: [], + }); + } else { + result = this.buildSelectQuery({ + table: aliasedTable(table, tableAlias), + fields: {}, + fieldsFlat: selection.map(({ field }) => ({ + path: [], + field: is(field, Column) ? aliasedTableColumn(field, tableAlias) : field, + })), + joins, + where, + limit, + offset, + orderBy, + setOperators: [], + }); + } + + return { + tableTsKey: tableConfig.tsName, + sql: result, + selection, + }; + } + + buildRelationalQueryWithoutLateralSubqueries({ + fullSchema, + schema, + tableNamesMap, + table, + tableConfig, + queryConfig: config, + tableAlias, + nestedQueryRelation, + joinOn, + }: { + fullSchema: Record; + schema: TablesRelationalConfig; + tableNamesMap: Record; + table: GoogleSqlTable; + tableConfig: TableRelationalConfig; + queryConfig: true | DBQueryConfig<'many', true>; + tableAlias: string; + nestedQueryRelation?: Relation; + joinOn?: SQL; + }): BuildRelationalQueryResult { + let selection: BuildRelationalQueryResult['selection'] = []; + let limit, offset, orderBy: GoogleSqlSelectConfig['orderBy'] = [], where; + + if (config === true) { + const selectionEntries = Object.entries(tableConfig.columns); + selection = selectionEntries.map(( + [key, value], + ) => ({ + dbKey: value.name, + tsKey: key, + field: aliasedTableColumn(value as GoogleSqlColumn, tableAlias), + relationTableTsKey: undefined, + isJson: false, + selection: [], + })); + } else { + const aliasedColumns = Object.fromEntries( + Object.entries(tableConfig.columns).map(([key, value]) => [key, aliasedTableColumn(value, tableAlias)]), + ); + + if (config.where) { + const whereSql = typeof config.where === 'function' + ? config.where(aliasedColumns, getOperators()) + : config.where; + where = whereSql && mapColumnsInSQLToAlias(whereSql, tableAlias); + } + + const fieldsSelection: { tsKey: string; value: GoogleSqlColumn | SQL.Aliased }[] = []; + let selectedColumns: string[] = []; + + // Figure out which columns to select + if (config.columns) { + let isIncludeMode = false; + + for (const [field, value] of Object.entries(config.columns)) { + if (value === undefined) { + continue; + } + + if (field in tableConfig.columns) { + if (!isIncludeMode && value === true) { + isIncludeMode = true; + } + selectedColumns.push(field); + } + } + + if (selectedColumns.length > 0) { + selectedColumns = isIncludeMode + ? selectedColumns.filter((c) => config.columns?.[c] === true) + : Object.keys(tableConfig.columns).filter((key) => !selectedColumns.includes(key)); + } + } else { + // Select all columns if selection is not specified + selectedColumns = Object.keys(tableConfig.columns); + } + + for (const field of selectedColumns) { + const column = tableConfig.columns[field]! as GoogleSqlColumn; + fieldsSelection.push({ tsKey: field, value: column }); + } + + let selectedRelations: { + tsKey: string; + queryConfig: true | DBQueryConfig<'many', false>; + relation: Relation; + }[] = []; + + // Figure out which relations to select + if (config.with) { + selectedRelations = Object.entries(config.with) + .filter((entry): entry is [typeof entry[0], NonNullable] => !!entry[1]) + .map(([tsKey, queryConfig]) => ({ tsKey, queryConfig, relation: tableConfig.relations[tsKey]! })); + } + + let extras; + + // Figure out which extras to select + if (config.extras) { + extras = typeof config.extras === 'function' + ? config.extras(aliasedColumns, { sql }) + : config.extras; + for (const [tsKey, value] of Object.entries(extras)) { + fieldsSelection.push({ + tsKey, + value: mapColumnsInAliasedSQLToAlias(value, tableAlias), + }); + } + } + + // Transform `fieldsSelection` into `selection` + // `fieldsSelection` shouldn't be used after this point + for (const { tsKey, value } of fieldsSelection) { + selection.push({ + dbKey: is(value, SQL.Aliased) ? value.fieldAlias : tableConfig.columns[tsKey]!.name, + tsKey, + field: is(value, Column) ? aliasedTableColumn(value, tableAlias) : value, + relationTableTsKey: undefined, + isJson: false, + selection: [], + }); + } + + let orderByOrig = typeof config.orderBy === 'function' + ? config.orderBy(aliasedColumns, getOrderByOperators()) + : config.orderBy ?? []; + if (!Array.isArray(orderByOrig)) { + orderByOrig = [orderByOrig]; + } + orderBy = orderByOrig.map((orderByValue) => { + if (is(orderByValue, Column)) { + return aliasedTableColumn(orderByValue, tableAlias) as GoogleSqlColumn; + } + return mapColumnsInSQLToAlias(orderByValue, tableAlias); + }); + + limit = config.limit; + offset = config.offset; + + // Process all relations + for ( + const { + tsKey: selectedRelationTsKey, + queryConfig: selectedRelationConfigValue, + relation, + } of selectedRelations + ) { + const normalizedRelation = normalizeRelation(schema, tableNamesMap, relation); + const relationTableName = getTableUniqueName(relation.referencedTable); + const relationTableTsName = tableNamesMap[relationTableName]!; + const relationTableAlias = `${tableAlias}_${selectedRelationTsKey}`; + const joinOn = and( + ...normalizedRelation.fields.map((field, i) => + eq( + aliasedTableColumn(normalizedRelation.references[i]!, relationTableAlias), + aliasedTableColumn(field, tableAlias), + ) + ), + ); + const builtRelation = this.buildRelationalQueryWithoutLateralSubqueries({ + fullSchema, + schema, + tableNamesMap, + table: fullSchema[relationTableTsName] as GoogleSqlTable, + tableConfig: schema[relationTableTsName]!, + queryConfig: is(relation, One) + ? (selectedRelationConfigValue === true + ? { limit: 1 } + : { ...selectedRelationConfigValue, limit: 1 }) + : selectedRelationConfigValue, + tableAlias: relationTableAlias, + joinOn, + nestedQueryRelation: relation, + }); + let fieldSql = sql`(${builtRelation.sql})`; + if (is(relation, Many)) { + fieldSql = sql`coalesce(${fieldSql}, json_array())`; + } + const field = fieldSql.as(selectedRelationTsKey); + selection.push({ + dbKey: selectedRelationTsKey, + tsKey: selectedRelationTsKey, + field, + relationTableTsKey: relationTableTsName, + isJson: true, + selection: builtRelation.selection, + }); + } + } + + if (selection.length === 0) { + throw new DrizzleError({ + message: + `No fields selected for table "${tableConfig.tsName}" ("${tableAlias}"). You need to have at least one item in "columns", "with" or "extras". If you need to select all columns, omit the "columns" key or set it to undefined.`, + }); + } + + let result; + + where = and(joinOn, where); + + if (nestedQueryRelation) { + let field = sql`json_array(${ + sql.join( + selection.map(({ field }) => + is(field, GoogleSqlColumn) + ? sql.identifier(this.casing.getColumnCasing(field)) + : is(field, SQL.Aliased) + ? field.sql + : field + ), + sql`, `, + ) + })`; + if (is(nestedQueryRelation, Many)) { + field = sql`json_arrayagg(${field})`; + } + const nestedSelection = [{ + dbKey: 'data', + tsKey: 'data', + field, + isJson: true, + relationTableTsKey: tableConfig.tsName, + selection, + }]; + + const needsSubquery = limit !== undefined || offset !== undefined || orderBy.length > 0; + + if (needsSubquery) { + result = this.buildSelectQuery({ + table: aliasedTable(table, tableAlias), + fields: {}, + fieldsFlat: [ + { + path: [], + field: sql.raw('*'), + }, + ...(orderBy.length > 0) + ? [{ + path: [], + field: sql`row_number() over (order by ${sql.join(orderBy, sql`, `)})`, + }] + : [], + ], + where, + limit, + offset, + setOperators: [], + }); + + where = undefined; + limit = undefined; + offset = undefined; + orderBy = undefined; + } else { + result = aliasedTable(table, tableAlias); + } + + result = this.buildSelectQuery({ + table: is(result, GoogleSqlTable) ? result : new Subquery(result, {}, tableAlias), + fields: {}, + fieldsFlat: nestedSelection.map(({ field }) => ({ + path: [], + field: is(field, Column) ? aliasedTableColumn(field, tableAlias) : field, + })), + where, + limit, + offset, + orderBy, + setOperators: [], + }); + } else { + result = this.buildSelectQuery({ + table: aliasedTable(table, tableAlias), + fields: {}, + fieldsFlat: selection.map(({ field }) => ({ + path: [], + field: is(field, Column) ? aliasedTableColumn(field, tableAlias) : field, + })), + where, + limit, + offset, + orderBy, + setOperators: [], + }); + } + + return { + tableTsKey: tableConfig.tsName, + sql: result, + selection, + }; + } +} diff --git a/drizzle-orm/src/googlesql/expressions.ts b/drizzle-orm/src/googlesql/expressions.ts new file mode 100644 index 0000000000..e6dfb85e5e --- /dev/null +++ b/drizzle-orm/src/googlesql/expressions.ts @@ -0,0 +1,25 @@ +import { bindIfParam } from '~/expressions.ts'; +import type { Placeholder, SQL, SQLChunk, SQLWrapper } from '~/sql/sql.ts'; +import { sql } from '~/sql/sql.ts'; +import type { GoogleSqlColumn } from './columns/index.ts'; + +export * from '~/expressions.ts'; + +export function concat(column: GoogleSqlColumn | SQL.Aliased, value: string | Placeholder | SQLWrapper): SQL { + return sql`${column} || ${bindIfParam(value, column)}`; +} + +export function substring( + column: GoogleSqlColumn | SQL.Aliased, + { from, for: _for }: { from?: number | Placeholder | SQLWrapper; for?: number | Placeholder | SQLWrapper }, +): SQL { + const chunks: SQLChunk[] = [sql`substring(`, column]; + if (from !== undefined) { + chunks.push(sql` from `, bindIfParam(from, column)); + } + if (_for !== undefined) { + chunks.push(sql` for `, bindIfParam(_for, column)); + } + chunks.push(sql`)`); + return sql.join(chunks); +} diff --git a/drizzle-orm/src/googlesql/foreign-keys.ts b/drizzle-orm/src/googlesql/foreign-keys.ts new file mode 100644 index 0000000000..7d39372a65 --- /dev/null +++ b/drizzle-orm/src/googlesql/foreign-keys.ts @@ -0,0 +1,126 @@ +import { entityKind } from '~/entity.ts'; +import { TableName } from '~/table.utils.ts'; +import type { AnyGoogleSqlColumn, GoogleSqlColumn } from './columns/index.ts'; +import type { GoogleSqlTable } from './table.ts'; + +export type UpdateDeleteAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default'; + +export type Reference = () => { + readonly name?: string; + readonly columns: GoogleSqlColumn[]; + readonly foreignTable: GoogleSqlTable; + readonly foreignColumns: GoogleSqlColumn[]; +}; + +export class ForeignKeyBuilder { + static readonly [entityKind]: string = 'GoogleSqlForeignKeyBuilder'; + + /** @internal */ + reference: Reference; + + /** @internal */ + _onUpdate: UpdateDeleteAction | undefined; + + /** @internal */ + _onDelete: UpdateDeleteAction | undefined; + + constructor( + config: () => { + name?: string; + columns: GoogleSqlColumn[]; + foreignColumns: GoogleSqlColumn[]; + }, + actions?: { + onUpdate?: UpdateDeleteAction; + onDelete?: UpdateDeleteAction; + } | undefined, + ) { + this.reference = () => { + const { name, columns, foreignColumns } = config(); + return { name, columns, foreignTable: foreignColumns[0]!.table as GoogleSqlTable, foreignColumns }; + }; + if (actions) { + this._onUpdate = actions.onUpdate; + this._onDelete = actions.onDelete; + } + } + + onUpdate(action: UpdateDeleteAction): this { + this._onUpdate = action; + return this; + } + + onDelete(action: UpdateDeleteAction): this { + this._onDelete = action; + return this; + } + + /** @internal */ + build(table: GoogleSqlTable): ForeignKey { + return new ForeignKey(table, this); + } +} + +export type AnyForeignKeyBuilder = ForeignKeyBuilder; + +export class ForeignKey { + static readonly [entityKind]: string = 'GoogleSqlForeignKey'; + + readonly reference: Reference; + readonly onUpdate: UpdateDeleteAction | undefined; + readonly onDelete: UpdateDeleteAction | undefined; + + constructor(readonly table: GoogleSqlTable, builder: ForeignKeyBuilder) { + this.reference = builder.reference; + this.onUpdate = builder._onUpdate; + this.onDelete = builder._onDelete; + } + + getName(): string { + const { name, columns, foreignColumns } = this.reference(); + const columnNames = columns.map((column) => column.name); + const foreignColumnNames = foreignColumns.map((column) => column.name); + const chunks = [ + this.table[TableName], + ...columnNames, + foreignColumns[0]!.table[TableName], + ...foreignColumnNames, + ]; + return name ?? `${chunks.join('_')}_fk`; + } +} + +type ColumnsWithTable< + TTableName extends string, + TColumns extends GoogleSqlColumn[], +> = { [Key in keyof TColumns]: AnyGoogleSqlColumn<{ tableName: TTableName }> }; + +export type GetColumnsTable = ( + TColumns extends GoogleSqlColumn ? TColumns + : TColumns extends GoogleSqlColumn[] ? TColumns[number] + : never +) extends AnyGoogleSqlColumn<{ tableName: infer TTableName extends string }> ? TTableName + : never; + +export function foreignKey< + TTableName extends string, + TForeignTableName extends string, + TColumns extends [AnyGoogleSqlColumn<{ tableName: TTableName }>, ...AnyGoogleSqlColumn<{ tableName: TTableName }>[]], +>( + config: { + name?: string; + columns: TColumns; + foreignColumns: ColumnsWithTable; + }, +): ForeignKeyBuilder { + function mappedConfig() { + const { name, columns, foreignColumns } = config; + return { + name, + columns, + foreignColumns, + }; + } + + return new ForeignKeyBuilder(mappedConfig); +} diff --git a/drizzle-orm/src/googlesql/index.ts b/drizzle-orm/src/googlesql/index.ts new file mode 100644 index 0000000000..204e0af3c4 --- /dev/null +++ b/drizzle-orm/src/googlesql/index.ts @@ -0,0 +1,17 @@ +export * from './alias.ts'; +export * from './checks.ts'; +export * from './columns/index.ts'; +export * from './db.ts'; +export * from './dialect.ts'; +export * from './foreign-keys.ts'; +export * from './indexes.ts'; +export * from './primary-keys.ts'; +export * from './query-builders/index.ts'; +export * from './schema.ts'; +export * from './session.ts'; +export * from './subquery.ts'; +export * from './table.ts'; +export * from './unique-constraint.ts'; +export * from './utils.ts'; +export * from './view-common.ts'; +export * from './view.ts'; diff --git a/drizzle-orm/src/googlesql/indexes.ts b/drizzle-orm/src/googlesql/indexes.ts new file mode 100644 index 0000000000..cb920ba814 --- /dev/null +++ b/drizzle-orm/src/googlesql/indexes.ts @@ -0,0 +1,108 @@ +import { entityKind } from '~/entity.ts'; +import type { SQL } from '~/sql/sql.ts'; +import type { AnyGoogleSqlColumn, GoogleSqlColumn } from './columns/index.ts'; +import type { GoogleSqlTable } from './table.ts'; + +interface IndexConfig { + name: string; + + columns: IndexColumn[]; + + /** + * If true, the index will be created as `create unique index` instead of `create index`. + */ + unique?: boolean; + + /** + * If set, the index will be created as `create index ... using { 'btree' | 'hash' }`. + */ + using?: 'btree' | 'hash'; + + /** + * If set, the index will be created as `create index ... algorythm { 'default' | 'inplace' | 'copy' }`. + */ + algorythm?: 'default' | 'inplace' | 'copy'; + + /** + * If set, adds locks to the index creation. + */ + lock?: 'default' | 'none' | 'shared' | 'exclusive'; +} + +export type IndexColumn = GoogleSqlColumn | SQL; + +export class IndexBuilderOn { + static readonly [entityKind]: string = 'GoogleSqlIndexBuilderOn'; + + constructor(private name: string, private unique: boolean) {} + + on(...columns: [IndexColumn, ...IndexColumn[]]): IndexBuilder { + return new IndexBuilder(this.name, columns, this.unique); + } +} + +export interface AnyIndexBuilder { + build(table: GoogleSqlTable): Index; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IndexBuilder extends AnyIndexBuilder {} + +export class IndexBuilder implements AnyIndexBuilder { + static readonly [entityKind]: string = 'GoogleSqlIndexBuilder'; + + /** @internal */ + config: IndexConfig; + + constructor(name: string, columns: IndexColumn[], unique: boolean) { + this.config = { + name, + columns, + unique, + }; + } + + using(using: IndexConfig['using']): this { + this.config.using = using; + return this; + } + + algorythm(algorythm: IndexConfig['algorythm']): this { + this.config.algorythm = algorythm; + return this; + } + + lock(lock: IndexConfig['lock']): this { + this.config.lock = lock; + return this; + } + + /** @internal */ + build(table: GoogleSqlTable): Index { + return new Index(this.config, table); + } +} + +export class Index { + static readonly [entityKind]: string = 'GoogleSqlIndex'; + + readonly config: IndexConfig & { table: GoogleSqlTable }; + + constructor(config: IndexConfig, table: GoogleSqlTable) { + this.config = { ...config, table }; + } +} + +export type GetColumnsTableName = TColumns extends + AnyGoogleSqlColumn<{ tableName: infer TTableName extends string }> | AnyGoogleSqlColumn< + { tableName: infer TTableName extends string } + >[] ? TTableName + : never; + +export function index(name: string): IndexBuilderOn { + return new IndexBuilderOn(name, false); +} + +export function uniqueIndex(name: string): IndexBuilderOn { + return new IndexBuilderOn(name, true); +} diff --git a/drizzle-orm/src/googlesql/primary-keys.ts b/drizzle-orm/src/googlesql/primary-keys.ts new file mode 100644 index 0000000000..fd695bb9e4 --- /dev/null +++ b/drizzle-orm/src/googlesql/primary-keys.ts @@ -0,0 +1,63 @@ +import { entityKind } from '~/entity.ts'; +import type { AnyGoogleSqlColumn, GoogleSqlColumn } from './columns/index.ts'; +import { GoogleSqlTable } from './table.ts'; + +export function primaryKey< + TTableName extends string, + TColumn extends AnyGoogleSqlColumn<{ tableName: TTableName }>, + TColumns extends AnyGoogleSqlColumn<{ tableName: TTableName }>[], +>(config: { name?: string; columns: [TColumn, ...TColumns] }): PrimaryKeyBuilder; +/** + * @deprecated: Please use primaryKey({ columns: [] }) instead of this function + * @param columns + */ +export function primaryKey< + TTableName extends string, + TColumns extends AnyGoogleSqlColumn<{ tableName: TTableName }>[], +>(...columns: TColumns): PrimaryKeyBuilder; +export function primaryKey(...config: any) { + if (config[0].columns) { + return new PrimaryKeyBuilder(config[0].columns, config[0].name); + } + return new PrimaryKeyBuilder(config); +} + +export class PrimaryKeyBuilder { + static readonly [entityKind]: string = 'GoogleSqlPrimaryKeyBuilder'; + + /** @internal */ + columns: GoogleSqlColumn[]; + + /** @internal */ + name?: string; + + constructor( + columns: GoogleSqlColumn[], + name?: string, + ) { + this.columns = columns; + this.name = name; + } + + /** @internal */ + build(table: GoogleSqlTable): PrimaryKey { + return new PrimaryKey(table, this.columns, this.name); + } +} + +export class PrimaryKey { + static readonly [entityKind]: string = 'GoogleSqlPrimaryKey'; + + readonly columns: GoogleSqlColumn[]; + readonly name?: string; + + constructor(readonly table: GoogleSqlTable, columns: GoogleSqlColumn[], name?: string) { + this.columns = columns; + this.name = name; + } + + getName(): string { + return this.name + ?? `${this.table[GoogleSqlTable.Symbol.Name]}_${this.columns.map((column) => column.name).join('_')}_pk`; + } +} diff --git a/drizzle-orm/src/googlesql/query-builders/count.ts b/drizzle-orm/src/googlesql/query-builders/count.ts new file mode 100644 index 0000000000..9c946b8690 --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/count.ts @@ -0,0 +1,79 @@ +import { entityKind } from '~/entity.ts'; +import { SQL, sql, type SQLWrapper } from '~/sql/sql.ts'; +import type { GoogleSqlSession } from '../session.ts'; +import type { GoogleSqlTable } from '../table.ts'; +import type { GoogleSqlViewBase } from '../view-base.ts'; + +export class GoogleSqlCountBuilder< + TSession extends GoogleSqlSession, +> extends SQL implements Promise, SQLWrapper { + private sql: SQL; + + static override readonly [entityKind] = 'GoogleSqlCountBuilder'; + [Symbol.toStringTag] = 'GoogleSqlCountBuilder'; + + private session: TSession; + + private static buildEmbeddedCount( + source: GoogleSqlTable | GoogleSqlViewBase | SQL | SQLWrapper, + filters?: SQL, + ): SQL { + return sql`(select count(*) from ${source}${sql.raw(' where ').if(filters)}${filters})`; + } + + private static buildCount( + source: GoogleSqlTable | GoogleSqlViewBase | SQL | SQLWrapper, + filters?: SQL, + ): SQL { + return sql`select count(*) as count from ${source}${sql.raw(' where ').if(filters)}${filters}`; + } + + constructor( + readonly params: { + source: GoogleSqlTable | GoogleSqlViewBase | SQL | SQLWrapper; + filters?: SQL; + session: TSession; + }, + ) { + super(GoogleSqlCountBuilder.buildEmbeddedCount(params.source, params.filters).queryChunks); + + this.mapWith(Number); + + this.session = params.session; + + this.sql = GoogleSqlCountBuilder.buildCount( + params.source, + params.filters, + ); + } + + then( + onfulfilled?: ((value: number) => TResult1 | PromiseLike) | null | undefined, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | null | undefined, + ): Promise { + return Promise.resolve(this.session.count(this.sql)) + .then( + onfulfilled, + onrejected, + ); + } + + catch( + onRejected?: ((reason: any) => never | PromiseLike) | null | undefined, + ): Promise { + return this.then(undefined, onRejected); + } + + finally(onFinally?: (() => void) | null | undefined): Promise { + return this.then( + (value) => { + onFinally?.(); + return value; + }, + (reason) => { + onFinally?.(); + throw reason; + }, + ); + } +} diff --git a/drizzle-orm/src/googlesql/query-builders/delete.ts b/drizzle-orm/src/googlesql/query-builders/delete.ts new file mode 100644 index 0000000000..beb6027e37 --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/delete.ts @@ -0,0 +1,207 @@ +import { entityKind } from '~/entity.ts'; +import type { GoogleSqlDialect } from '~/googlesql/dialect.ts'; +import type { + AnyGoogleSqlQueryResultHKT, + GoogleSqlPreparedQueryConfig, + GoogleSqlQueryResultHKT, + GoogleSqlQueryResultKind, + GoogleSqlSession, + PreparedQueryHKTBase, + PreparedQueryKind, +} from '~/googlesql/session.ts'; +import type { GoogleSqlTable } from '~/googlesql/table.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; +import type { Placeholder, Query, SQL, SQLWrapper } from '~/sql/sql.ts'; +import type { Subquery } from '~/subquery.ts'; +import { Table } from '~/table.ts'; +import type { ValueOrArray } from '~/utils.ts'; +import type { GoogleSqlColumn } from '../columns/common.ts'; +import type { SelectedFieldsOrdered } from './select.types.ts'; + +export type GoogleSqlDeleteWithout< + T extends AnyGoogleSqlDeleteBase, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true ? T + : Omit< + GoogleSqlDeleteBase< + T['_']['table'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'], + TDynamic, + T['_']['excludedMethods'] | K + >, + T['_']['excludedMethods'] | K + >; + +export type GoogleSqlDelete< + TTable extends GoogleSqlTable = GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT = AnyGoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, +> = GoogleSqlDeleteBase; + +export interface GoogleSqlDeleteConfig { + where?: SQL | undefined; + limit?: number | Placeholder; + orderBy?: (GoogleSqlColumn | SQL | SQL.Aliased)[]; + table: GoogleSqlTable; + returning?: SelectedFieldsOrdered; + withList?: Subquery[]; +} + +export type GoogleSqlDeletePrepare = PreparedQueryKind< + T['_']['preparedQueryHKT'], + GoogleSqlPreparedQueryConfig & { + execute: GoogleSqlQueryResultKind; + iterator: never; + }, + true +>; + +type GoogleSqlDeleteDynamic = GoogleSqlDelete< + T['_']['table'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'] +>; + +type AnyGoogleSqlDeleteBase = GoogleSqlDeleteBase; + +export interface GoogleSqlDeleteBase< + TTable extends GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, +> extends QueryPromise> { + readonly _: { + readonly table: TTable; + readonly queryResult: TQueryResult; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + }; +} + +export class GoogleSqlDeleteBase< + TTable extends GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TPreparedQueryHKT extends PreparedQueryHKTBase, + TDynamic extends boolean = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TExcludedMethods extends string = never, +> extends QueryPromise> implements SQLWrapper { + static override readonly [entityKind]: string = 'GoogleSqlDelete'; + + private config: GoogleSqlDeleteConfig; + + constructor( + private table: TTable, + private session: GoogleSqlSession, + private dialect: GoogleSqlDialect, + withList?: Subquery[], + ) { + super(); + this.config = { table, withList }; + } + + /** + * Adds a `where` clause to the query. + * + * Calling this method will delete only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param where the `where` clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be deleted. + * + * ```ts + * // Delete all cars with green color + * db.delete(cars).where(eq(cars.color, 'green')); + * // or + * db.delete(cars).where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Delete all BMW cars with a green color + * db.delete(cars).where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Delete all cars with the green or blue color + * db.delete(cars).where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ + where(where: SQL | undefined): GoogleSqlDeleteWithout { + this.config.where = where; + return this as any; + } + + orderBy( + builder: (deleteTable: TTable) => ValueOrArray, + ): GoogleSqlDeleteWithout; + orderBy(...columns: (GoogleSqlColumn | SQL | SQL.Aliased)[]): GoogleSqlDeleteWithout; + orderBy( + ...columns: + | [(deleteTable: TTable) => ValueOrArray] + | (GoogleSqlColumn | SQL | SQL.Aliased)[] + ): GoogleSqlDeleteWithout { + if (typeof columns[0] === 'function') { + const orderBy = columns[0]( + new Proxy( + this.config.table[Table.Symbol.Columns], + new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + ) as any, + ); + + const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy]; + this.config.orderBy = orderByArray; + } else { + const orderByArray = columns as (GoogleSqlColumn | SQL | SQL.Aliased)[]; + this.config.orderBy = orderByArray; + } + return this as any; + } + + limit(limit: number | Placeholder): GoogleSqlDeleteWithout { + this.config.limit = limit; + return this as any; + } + + /** @internal */ + getSQL(): SQL { + return this.dialect.buildDeleteQuery(this.config); + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + prepare(): GoogleSqlDeletePrepare { + return this.session.prepareQuery( + this.dialect.sqlToQuery(this.getSQL()), + this.config.returning, + ) as GoogleSqlDeletePrepare; + } + + override execute: ReturnType['execute'] = (placeholderValues) => { + return this.prepare().execute(placeholderValues); + }; + + private createIterator = (): ReturnType['iterator'] => { + const self = this; + return async function*(placeholderValues) { + yield* self.prepare().iterator(placeholderValues); + }; + }; + + iterator = this.createIterator(); + + $dynamic(): GoogleSqlDeleteDynamic { + return this as any; + } +} diff --git a/drizzle-orm/src/googlesql/query-builders/index.ts b/drizzle-orm/src/googlesql/query-builders/index.ts new file mode 100644 index 0000000000..16f0e1d4d9 --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/index.ts @@ -0,0 +1,6 @@ +export * from './delete.ts'; +export * from './insert.ts'; +export * from './query-builder.ts'; +export * from './select.ts'; +export * from './select.types.ts'; +export * from './update.ts'; diff --git a/drizzle-orm/src/googlesql/query-builders/insert.ts b/drizzle-orm/src/googlesql/query-builders/insert.ts new file mode 100644 index 0000000000..8bd043e717 --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/insert.ts @@ -0,0 +1,338 @@ +import { entityKind, is } from '~/entity.ts'; +import type { GoogleSqlDialect } from '~/googlesql/dialect.ts'; +import type { + AnyGoogleSqlQueryResultHKT, + GoogleSqlPreparedQueryConfig, + GoogleSqlQueryResultHKT, + GoogleSqlQueryResultKind, + GoogleSqlSession, + PreparedQueryHKTBase, + PreparedQueryKind, +} from '~/googlesql/session.ts'; +import type { GoogleSqlTable } from '~/googlesql/table.ts'; +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { RunnableQuery } from '~/runnable-query.ts'; +import type { Placeholder, Query, SQLWrapper } from '~/sql/sql.ts'; +import { Param, SQL, sql } from '~/sql/sql.ts'; +import type { InferModelFromColumns } from '~/table.ts'; +import { Columns, Table } from '~/table.ts'; +import { haveSameKeys, mapUpdateSet } from '~/utils.ts'; +import type { AnyGoogleSqlColumn } from '../columns/common.ts'; +import { QueryBuilder } from './query-builder.ts'; +import type { SelectedFieldsOrdered } from './select.types.ts'; +import type { GoogleSqlUpdateSetSource } from './update.ts'; + +export interface GoogleSqlInsertConfig { + table: TTable; + values: Record[] | GoogleSqlInsertSelectQueryBuilder | SQL; + ignore: boolean; + onConflict?: SQL; + returning?: SelectedFieldsOrdered; + select?: boolean; +} + +export type AnyGoogleSqlInsertConfig = GoogleSqlInsertConfig; + +export type GoogleSqlInsertValue = + & { + [Key in keyof TTable['$inferInsert']]: TTable['$inferInsert'][Key] | SQL | Placeholder; + } + & {}; + +export type GoogleSqlInsertSelectQueryBuilder = TypedQueryBuilder< + { [K in keyof TTable['$inferInsert']]: AnyGoogleSqlColumn | SQL | SQL.Aliased | TTable['$inferInsert'][K] } +>; + +export class GoogleSqlInsertBuilder< + TTable extends GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, +> { + static readonly [entityKind]: string = 'GoogleSqlInsertBuilder'; + + private shouldIgnore = false; + + constructor( + private table: TTable, + private session: GoogleSqlSession, + private dialect: GoogleSqlDialect, + ) {} + + ignore(): this { + this.shouldIgnore = true; + return this; + } + + values(value: GoogleSqlInsertValue): GoogleSqlInsertBase; + values(values: GoogleSqlInsertValue[]): GoogleSqlInsertBase; + values( + values: GoogleSqlInsertValue | GoogleSqlInsertValue[], + ): GoogleSqlInsertBase { + values = Array.isArray(values) ? values : [values]; + if (values.length === 0) { + throw new Error('values() must be called with at least one value'); + } + const mappedValues = values.map((entry) => { + const result: Record = {}; + const cols = this.table[Table.Symbol.Columns]; + for (const colKey of Object.keys(entry)) { + const colValue = entry[colKey as keyof typeof entry]; + result[colKey] = is(colValue, SQL) ? colValue : new Param(colValue, cols[colKey]); + } + return result; + }); + + return new GoogleSqlInsertBase(this.table, mappedValues, this.shouldIgnore, this.session, this.dialect); + } + + select( + selectQuery: (qb: QueryBuilder) => GoogleSqlInsertSelectQueryBuilder, + ): GoogleSqlInsertBase; + select(selectQuery: (qb: QueryBuilder) => SQL): GoogleSqlInsertBase; + select(selectQuery: SQL): GoogleSqlInsertBase; + select( + selectQuery: GoogleSqlInsertSelectQueryBuilder, + ): GoogleSqlInsertBase; + select( + selectQuery: + | SQL + | GoogleSqlInsertSelectQueryBuilder + | ((qb: QueryBuilder) => GoogleSqlInsertSelectQueryBuilder | SQL), + ): GoogleSqlInsertBase { + const select = typeof selectQuery === 'function' ? selectQuery(new QueryBuilder()) : selectQuery; + + if ( + !is(select, SQL) + && !haveSameKeys(this.table[Columns], select._.selectedFields) + ) { + throw new Error( + 'Insert select error: selected fields are not the same or are in a different order compared to the table definition', + ); + } + + return new GoogleSqlInsertBase(this.table, select, this.shouldIgnore, this.session, this.dialect, true); + } +} + +export type GoogleSqlInsertWithout = + TDynamic extends true ? T + : Omit< + GoogleSqlInsertBase< + T['_']['table'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'], + T['_']['returning'], + TDynamic, + T['_']['excludedMethods'] | '$returning' + >, + T['_']['excludedMethods'] | K + >; + +export type GoogleSqlInsertDynamic = GoogleSqlInsert< + T['_']['table'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'], + T['_']['returning'] +>; + +export type GoogleSqlInsertPrepare< + T extends AnyGoogleSqlInsert, + TReturning extends Record | undefined = undefined, +> = PreparedQueryKind< + T['_']['preparedQueryHKT'], + GoogleSqlPreparedQueryConfig & { + execute: TReturning extends undefined ? GoogleSqlQueryResultKind : TReturning[]; + iterator: never; + }, + true +>; + +export type GoogleSqlInsertOnDuplicateKeyUpdateConfig = { + set: GoogleSqlUpdateSetSource; +}; + +export type GoogleSqlInsert< + TTable extends GoogleSqlTable = GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT = AnyGoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TReturning extends Record | undefined = Record | undefined, +> = GoogleSqlInsertBase; + +export type GoogleSqlInsertReturning< + T extends AnyGoogleSqlInsert, + TDynamic extends boolean, +> = GoogleSqlInsertBase< + T['_']['table'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'], + InferModelFromColumns>, + TDynamic, + T['_']['excludedMethods'] | '$returning' +>; + +export type AnyGoogleSqlInsert = GoogleSqlInsertBase; + +export interface GoogleSqlInsertBase< + TTable extends GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TReturning extends Record | undefined = undefined, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, +> extends + QueryPromise : TReturning[]>, + RunnableQuery< + TReturning extends undefined ? GoogleSqlQueryResultKind : TReturning[], + 'googlesql' + >, + SQLWrapper +{ + readonly _: { + readonly dialect: 'googlesql'; + readonly table: TTable; + readonly queryResult: TQueryResult; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly returning: TReturning; + readonly result: TReturning extends undefined ? GoogleSqlQueryResultKind : TReturning[]; + }; +} + +export type PrimaryKeyKeys> = { + [K in keyof T]: T[K]['_']['isPrimaryKey'] extends true ? T[K]['_']['isAutoincrement'] extends true ? K + : T[K]['_']['hasRuntimeDefault'] extends true ? T[K]['_']['isPrimaryKey'] extends true ? K : never + : never + : T[K]['_']['hasRuntimeDefault'] extends true ? T[K]['_']['isPrimaryKey'] extends true ? K : never + : never; +}[keyof T]; + +export type GetPrimarySerialOrDefaultKeys> = { + [K in PrimaryKeyKeys]: T[K]; +}; + +export class GoogleSqlInsertBase< + TTable extends GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TPreparedQueryHKT extends PreparedQueryHKTBase, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TReturning extends Record | undefined = undefined, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TDynamic extends boolean = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TExcludedMethods extends string = never, +> extends QueryPromise : TReturning[]> + implements + RunnableQuery< + TReturning extends undefined ? GoogleSqlQueryResultKind : TReturning[], + 'googlesql' + >, + SQLWrapper +{ + static override readonly [entityKind]: string = 'GoogleSqlInsert'; + + declare protected $table: TTable; + + private config: GoogleSqlInsertConfig; + + constructor( + table: TTable, + values: GoogleSqlInsertConfig['values'], + ignore: boolean, + private session: GoogleSqlSession, + private dialect: GoogleSqlDialect, + select?: boolean, + ) { + super(); + this.config = { table, values: values as any, select, ignore }; + } + + /** + * Adds an `on duplicate key update` clause to the query. + * + * Calling this method will update the row if any unique index conflicts. MySQL will automatically determine the conflict target based on the primary key and unique indexes. + * + * See docs: {@link https://orm.drizzle.team/docs/insert#on-duplicate-key-update} + * + * @param config The `set` clause + * + * @example + * ```ts + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW'}) + * .onDuplicateKeyUpdate({ set: { brand: 'Porsche' }}); + * ``` + * + * While MySQL does not directly support doing nothing on conflict, you can perform a no-op by setting any column's value to itself and achieve the same effect: + * + * ```ts + * import { sql } from 'drizzle-orm'; + * + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onDuplicateKeyUpdate({ set: { id: sql`id` } }); + * ``` + */ + onDuplicateKeyUpdate( + config: GoogleSqlInsertOnDuplicateKeyUpdateConfig, + ): GoogleSqlInsertWithout { + const setSql = this.dialect.buildUpdateSet(this.config.table, mapUpdateSet(this.config.table, config.set)); + this.config.onConflict = sql`update ${setSql}`; + return this as any; + } + + $returningId(): GoogleSqlInsertWithout< + GoogleSqlInsertReturning, + TDynamic, + '$returningId' + > { + const returning: SelectedFieldsOrdered = []; + for (const [key, value] of Object.entries(this.config.table[Table.Symbol.Columns])) { + if (value.primary) { + returning.push({ field: value, path: [key] }); + } + } + this.config.returning = returning; + return this as any; + } + + /** @internal */ + getSQL(): SQL { + return this.dialect.buildInsertQuery(this.config).sql; + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + prepare(): GoogleSqlInsertPrepare { + const { sql, generatedIds } = this.dialect.buildInsertQuery(this.config); + return this.session.prepareQuery( + this.dialect.sqlToQuery(sql), + undefined, + undefined, + generatedIds, + this.config.returning, + ) as GoogleSqlInsertPrepare; + } + + override execute: ReturnType['execute'] = (placeholderValues) => { + return this.prepare().execute(placeholderValues); + }; + + private createIterator = (): ReturnType['iterator'] => { + const self = this; + return async function*(placeholderValues) { + yield* self.prepare().iterator(placeholderValues); + }; + }; + + iterator = this.createIterator(); + + $dynamic(): GoogleSqlInsertDynamic { + return this as any; + } +} diff --git a/drizzle-orm/src/googlesql/query-builders/query-builder.ts b/drizzle-orm/src/googlesql/query-builders/query-builder.ts new file mode 100644 index 0000000000..83feeccd7d --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/query-builder.ts @@ -0,0 +1,116 @@ +import { entityKind, is } from '~/entity.ts'; +import type { GoogleSqlDialectConfig } from '~/googlesql/dialect.ts'; +import { GoogleSqlDialect } from '~/googlesql/dialect.ts'; +import type { WithBuilder } from '~/googlesql/subquery.ts'; +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; +import type { ColumnsSelection, SQL } from '~/sql/sql.ts'; +import { WithSubquery } from '~/subquery.ts'; +import { GoogleSqlSelectBuilder } from './select.ts'; +import type { SelectedFields } from './select.types.ts'; + +export class QueryBuilder { + static readonly [entityKind]: string = 'GoogleSqlQueryBuilder'; + + private dialect: GoogleSqlDialect | undefined; + private dialectConfig: GoogleSqlDialectConfig | undefined; + + constructor(dialect?: GoogleSqlDialect | GoogleSqlDialectConfig) { + this.dialect = is(dialect, GoogleSqlDialect) ? dialect : undefined; + this.dialectConfig = is(dialect, GoogleSqlDialect) ? undefined : dialect; + } + + $with: WithBuilder = (alias: string, selection?: ColumnsSelection) => { + const queryBuilder = this; + const as = ( + qb: + | TypedQueryBuilder + | SQL + | ((qb: QueryBuilder) => TypedQueryBuilder | SQL), + ) => { + if (typeof qb === 'function') { + qb = qb(queryBuilder); + } + + return new Proxy( + new WithSubquery( + qb.getSQL(), + selection ?? ('getSelectedFields' in qb ? qb.getSelectedFields() ?? {} : {}) as SelectedFields, + alias, + true, + ), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), + ) as any; + }; + return { as }; + }; + + with(...queries: WithSubquery[]) { + const self = this; + + function select(): GoogleSqlSelectBuilder; + function select( + fields: TSelection, + ): GoogleSqlSelectBuilder; + function select( + fields?: TSelection, + ): GoogleSqlSelectBuilder { + return new GoogleSqlSelectBuilder({ + fields: fields ?? undefined, + session: undefined, + dialect: self.getDialect(), + withList: queries, + }); + } + + function selectDistinct(): GoogleSqlSelectBuilder; + function selectDistinct( + fields: TSelection, + ): GoogleSqlSelectBuilder; + function selectDistinct( + fields?: TSelection, + ): GoogleSqlSelectBuilder { + return new GoogleSqlSelectBuilder({ + fields: fields ?? undefined, + session: undefined, + dialect: self.getDialect(), + withList: queries, + distinct: true, + }); + } + + return { select, selectDistinct }; + } + + select(): GoogleSqlSelectBuilder; + select(fields: TSelection): GoogleSqlSelectBuilder; + select( + fields?: TSelection, + ): GoogleSqlSelectBuilder { + return new GoogleSqlSelectBuilder({ fields: fields ?? undefined, session: undefined, dialect: this.getDialect() }); + } + + selectDistinct(): GoogleSqlSelectBuilder; + selectDistinct( + fields: TSelection, + ): GoogleSqlSelectBuilder; + selectDistinct( + fields?: TSelection, + ): GoogleSqlSelectBuilder { + return new GoogleSqlSelectBuilder({ + fields: fields ?? undefined, + session: undefined, + dialect: this.getDialect(), + distinct: true, + }); + } + + // Lazy load dialect to avoid circular dependency + private getDialect() { + if (!this.dialect) { + this.dialect = new GoogleSqlDialect(this.dialectConfig); + } + + return this.dialect; + } +} diff --git a/drizzle-orm/src/googlesql/query-builders/query.ts b/drizzle-orm/src/googlesql/query-builders/query.ts new file mode 100644 index 0000000000..70363dd626 --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/query.ts @@ -0,0 +1,157 @@ +import { entityKind } from '~/entity.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import { + type BuildQueryResult, + type BuildRelationalQueryResult, + type DBQueryConfig, + mapRelationalRow, + type TableRelationalConfig, + type TablesRelationalConfig, +} from '~/relations.ts'; +import type { Query, QueryWithTypings, SQL } from '~/sql/sql.ts'; +import type { KnownKeysOnly } from '~/utils.ts'; +import type { GoogleSqlDialect } from '../dialect.ts'; +import type { + GoogleSqlPreparedQueryConfig, + GoogleSqlSession, + Mode, + PreparedQueryHKTBase, + PreparedQueryKind, +} from '../session.ts'; +import type { GoogleSqlTable } from '../table.ts'; + +export class RelationalQueryBuilder< + TPreparedQueryHKT extends PreparedQueryHKTBase, + TSchema extends TablesRelationalConfig, + TFields extends TableRelationalConfig, +> { + static readonly [entityKind]: string = 'GoogleSqlRelationalQueryBuilder'; + + constructor( + private fullSchema: Record, + private schema: TSchema, + private tableNamesMap: Record, + private table: GoogleSqlTable, + private tableConfig: TableRelationalConfig, + private dialect: GoogleSqlDialect, + private session: GoogleSqlSession, + private mode: Mode, + ) {} + + findMany>( + config?: KnownKeysOnly>, + ): GoogleSqlRelationalQuery[]> { + return new GoogleSqlRelationalQuery( + this.fullSchema, + this.schema, + this.tableNamesMap, + this.table, + this.tableConfig, + this.dialect, + this.session, + config ? (config as DBQueryConfig<'many', true>) : {}, + 'many', + this.mode, + ); + } + + findFirst, 'limit'>>( + config?: KnownKeysOnly, 'limit'>>, + ): GoogleSqlRelationalQuery | undefined> { + return new GoogleSqlRelationalQuery( + this.fullSchema, + this.schema, + this.tableNamesMap, + this.table, + this.tableConfig, + this.dialect, + this.session, + config ? { ...(config as DBQueryConfig<'many', true> | undefined), limit: 1 } : { limit: 1 }, + 'first', + this.mode, + ); + } +} + +export class GoogleSqlRelationalQuery< + TPreparedQueryHKT extends PreparedQueryHKTBase, + TResult, +> extends QueryPromise { + static override readonly [entityKind]: string = 'GoogleSqlRelationalQuery'; + + declare protected $brand: 'GoogleSqlRelationalQuery'; + + constructor( + private fullSchema: Record, + private schema: TablesRelationalConfig, + private tableNamesMap: Record, + private table: GoogleSqlTable, + private tableConfig: TableRelationalConfig, + private dialect: GoogleSqlDialect, + private session: GoogleSqlSession, + private config: DBQueryConfig<'many', true> | true, + private queryMode: 'many' | 'first', + private mode?: Mode, + ) { + super(); + } + + prepare() { + const { query, builtQuery } = this._toSQL(); + return this.session.prepareQuery( + builtQuery, + undefined, + (rawRows) => { + const rows = rawRows.map((row) => mapRelationalRow(this.schema, this.tableConfig, row, query.selection)); + if (this.queryMode === 'first') { + return rows[0] as TResult; + } + return rows as TResult; + }, + ) as PreparedQueryKind; + } + + private _getQuery() { + const query = this.mode === 'planetscale' + ? this.dialect.buildRelationalQueryWithoutLateralSubqueries({ + fullSchema: this.fullSchema, + schema: this.schema, + tableNamesMap: this.tableNamesMap, + table: this.table, + tableConfig: this.tableConfig, + queryConfig: this.config, + tableAlias: this.tableConfig.tsName, + }) + : this.dialect.buildRelationalQuery({ + fullSchema: this.fullSchema, + schema: this.schema, + tableNamesMap: this.tableNamesMap, + table: this.table, + tableConfig: this.tableConfig, + queryConfig: this.config, + tableAlias: this.tableConfig.tsName, + }); + return query; + } + + private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: QueryWithTypings } { + const query = this._getQuery(); + + const builtQuery = this.dialect.sqlToQuery(query.sql as SQL); + + return { builtQuery, query }; + } + + /** @internal */ + getSQL(): SQL { + return this._getQuery().sql as SQL; + } + + toSQL(): Query { + return this._toSQL().builtQuery; + } + + override execute(): Promise { + return this.prepare().execute(); + } +} diff --git a/drizzle-orm/src/googlesql/query-builders/select.ts b/drizzle-orm/src/googlesql/query-builders/select.ts new file mode 100644 index 0000000000..b6ef7a1934 --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/select.ts @@ -0,0 +1,1290 @@ +import { entityKind, is } from '~/entity.ts'; +import type { GoogleSqlColumn } from '~/googlesql/columns/index.ts'; +import type { GoogleSqlDialect } from '~/googlesql/dialect.ts'; +import type { GoogleSqlPreparedQueryConfig, GoogleSqlSession, PreparedQueryHKTBase } from '~/googlesql/session.ts'; +import type { SubqueryWithSelection } from '~/googlesql/subquery.ts'; +import { GoogleSqlTable } from '~/googlesql/table.ts'; +import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { + BuildSubquerySelection, + GetSelectTableName, + GetSelectTableSelection, + JoinNullability, + JoinType, + SelectMode, + SelectResult, + SetOperator, +} from '~/query-builders/select.types.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; +import type { ColumnsSelection, Placeholder, Query } from '~/sql/sql.ts'; +import { SQL, View } from '~/sql/sql.ts'; +import { Subquery } from '~/subquery.ts'; +import { Table } from '~/table.ts'; +import type { ValueOrArray } from '~/utils.ts'; +import { applyMixins, getTableColumns, getTableLikeName, haveSameKeys, orderSelectedFields } from '~/utils.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import type { IndexBuilder } from '../indexes.ts'; +import { convertIndexToString, toArray } from '../utils.ts'; +import { GoogleSqlViewBase } from '../view-base.ts'; +import type { + AnyGoogleSqlSelect, + CreateGoogleSqlSelectFromBuilderMode, + GetGoogleSqlSetOperators, + GoogleSqlCreateSetOperatorFn, + GoogleSqlJoinFn, + GoogleSqlSelectConfig, + GoogleSqlSelectDynamic, + GoogleSqlSelectHKT, + GoogleSqlSelectHKTBase, + GoogleSqlSelectPrepare, + GoogleSqlSelectWithout, + GoogleSqlSetOperatorExcludedMethods, + GoogleSqlSetOperatorWithResult, + LockConfig, + LockStrength, + SelectedFields, + SetOperatorRightSelect, +} from './select.types.ts'; + +export type IndexForHint = IndexBuilder | string; + +export type IndexConfig = { + useIndex?: IndexForHint | IndexForHint[]; + forceIndex?: IndexForHint | IndexForHint[]; + ignoreIndex?: IndexForHint | IndexForHint[]; +}; + +export class GoogleSqlSelectBuilder< + TSelection extends SelectedFields | undefined, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TBuilderMode extends 'db' | 'qb' = 'db', +> { + static readonly [entityKind]: string = 'GoogleSqlSelectBuilder'; + + private fields: TSelection; + private session: GoogleSqlSession | undefined; + private dialect: GoogleSqlDialect; + private withList: Subquery[] = []; + private distinct: boolean | undefined; + + constructor( + config: { + fields: TSelection; + session: GoogleSqlSession | undefined; + dialect: GoogleSqlDialect; + withList?: Subquery[]; + distinct?: boolean; + }, + ) { + this.fields = config.fields; + this.session = config.session; + this.dialect = config.dialect; + if (config.withList) { + this.withList = config.withList; + } + this.distinct = config.distinct; + } + + from( + source: TFrom, + onIndex?: TFrom extends GoogleSqlTable ? IndexConfig + : 'Index hint configuration is allowed only for GoogleSqlTable and not for subqueries or views', + ): CreateGoogleSqlSelectFromBuilderMode< + TBuilderMode, + GetSelectTableName, + TSelection extends undefined ? GetSelectTableSelection : TSelection, + TSelection extends undefined ? 'single' : 'partial', + TPreparedQueryHKT + > { + const isPartialSelect = !!this.fields; + + let fields: SelectedFields; + if (this.fields) { + fields = this.fields; + } else if (is(source, Subquery)) { + // This is required to use the proxy handler to get the correct field values from the subquery + fields = Object.fromEntries( + Object.keys(source._.selectedFields).map(( + key, + ) => [key, source[key as unknown as keyof typeof source] as unknown as SelectedFields[string]]), + ); + } else if (is(source, GoogleSqlViewBase)) { + fields = source[ViewBaseConfig].selectedFields as SelectedFields; + } else if (is(source, SQL)) { + fields = {}; + } else { + fields = getTableColumns(source); + } + + let useIndex: string[] = []; + let forceIndex: string[] = []; + let ignoreIndex: string[] = []; + if (is(source, GoogleSqlTable) && onIndex && typeof onIndex !== 'string') { + if (onIndex.useIndex) { + useIndex = convertIndexToString(toArray(onIndex.useIndex)); + } + if (onIndex.forceIndex) { + forceIndex = convertIndexToString(toArray(onIndex.forceIndex)); + } + if (onIndex.ignoreIndex) { + ignoreIndex = convertIndexToString(toArray(onIndex.ignoreIndex)); + } + } + + return new GoogleSqlSelectBase( + { + table: source, + fields, + isPartialSelect, + session: this.session, + dialect: this.dialect, + withList: this.withList, + distinct: this.distinct, + useIndex, + forceIndex, + ignoreIndex, + }, + ) as any; + } +} + +export abstract class GoogleSqlSelectQueryBuilderBase< + THKT extends GoogleSqlSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends TypedQueryBuilder { + static override readonly [entityKind]: string = 'GoogleSqlSelectQueryBuilder'; + + override readonly _: { + readonly hkt: THKT; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + + protected config: GoogleSqlSelectConfig; + protected joinsNotNullableMap: Record; + private tableName: string | undefined; + private isPartialSelect: boolean; + /** @internal */ + readonly session: GoogleSqlSession | undefined; + protected dialect: GoogleSqlDialect; + + constructor( + { table, fields, isPartialSelect, session, dialect, withList, distinct, useIndex, forceIndex, ignoreIndex }: { + table: GoogleSqlSelectConfig['table']; + fields: GoogleSqlSelectConfig['fields']; + isPartialSelect: boolean; + session: GoogleSqlSession | undefined; + dialect: GoogleSqlDialect; + withList: Subquery[]; + distinct: boolean | undefined; + useIndex?: string[]; + forceIndex?: string[]; + ignoreIndex?: string[]; + }, + ) { + super(); + this.config = { + withList, + table, + fields: { ...fields }, + distinct, + setOperators: [], + useIndex, + forceIndex, + ignoreIndex, + }; + this.isPartialSelect = isPartialSelect; + this.session = session; + this.dialect = dialect; + this._ = { + selectedFields: fields as TSelectedFields, + } as this['_']; + this.tableName = getTableLikeName(table); + this.joinsNotNullableMap = typeof this.tableName === 'string' ? { [this.tableName]: true } : {}; + } + + private createJoin( + joinType: TJoinType, + ): GoogleSqlJoinFn { + return < + TJoinedTable extends GoogleSqlTable | Subquery | GoogleSqlViewBase | SQL, + >( + table: GoogleSqlTable | Subquery | GoogleSqlViewBase | SQL, + on: ((aliases: TSelection) => SQL | undefined) | SQL | undefined, + onIndex?: TJoinedTable extends GoogleSqlTable ? IndexConfig + : 'Index hint configuration is allowed only for GoogleSqlTable and not for subqueries or views', + ) => { + const baseTableName = this.tableName; + const tableName = getTableLikeName(table); + + if (typeof tableName === 'string' && this.config.joins?.some((join) => join.alias === tableName)) { + throw new Error(`Alias "${tableName}" is already used in this query`); + } + + if (!this.isPartialSelect) { + // If this is the first join and this is not a partial select and we're not selecting from raw SQL, "move" the fields from the main table to the nested object + if (Object.keys(this.joinsNotNullableMap).length === 1 && typeof baseTableName === 'string') { + this.config.fields = { + [baseTableName]: this.config.fields, + }; + } + if (typeof tableName === 'string' && !is(table, SQL)) { + const selection = is(table, Subquery) + ? table._.selectedFields + : is(table, View) + ? table[ViewBaseConfig].selectedFields + : table[Table.Symbol.Columns]; + this.config.fields[tableName] = selection; + } + } + + if (typeof on === 'function') { + on = on( + new Proxy( + this.config.fields, + new SelectionProxyHandler({ sqlAliasedBehavior: 'sql', sqlBehavior: 'sql' }), + ) as TSelection, + ); + } + + if (!this.config.joins) { + this.config.joins = []; + } + + let useIndex: string[] = []; + let forceIndex: string[] = []; + let ignoreIndex: string[] = []; + if (is(table, GoogleSqlTable) && onIndex && typeof onIndex !== 'string') { + if (onIndex.useIndex) { + useIndex = convertIndexToString(toArray(onIndex.useIndex)); + } + if (onIndex.forceIndex) { + forceIndex = convertIndexToString(toArray(onIndex.forceIndex)); + } + if (onIndex.ignoreIndex) { + ignoreIndex = convertIndexToString(toArray(onIndex.ignoreIndex)); + } + } + + this.config.joins.push({ on, table, joinType, alias: tableName, useIndex, forceIndex, ignoreIndex }); + + if (typeof tableName === 'string') { + switch (joinType) { + case 'left': { + this.joinsNotNullableMap[tableName] = false; + break; + } + case 'right': { + this.joinsNotNullableMap = Object.fromEntries( + Object.entries(this.joinsNotNullableMap).map(([key]) => [key, false]), + ); + this.joinsNotNullableMap[tableName] = true; + break; + } + case 'inner': { + this.joinsNotNullableMap[tableName] = true; + break; + } + case 'full': { + this.joinsNotNullableMap = Object.fromEntries( + Object.entries(this.joinsNotNullableMap).map(([key]) => [key, false]), + ); + this.joinsNotNullableMap[tableName] = false; + break; + } + } + } + + return this as any; + }; + } + + /** + * Executes a `left join` operation by adding another table to the current query. + * + * Calling this method associates each row of the table with the corresponding row from the joined table, if a match is found. If no matching row exists, it sets all columns of the joined table to null. + * + * See docs: {@link https://orm.drizzle.team/docs/joins#left-join} + * + * @param table the table to join. + * @param on the `on` clause. + * + * @example + * + * ```ts + * // Select all users and their pets + * const usersWithPets: { user: User; pets: Pet | null }[] = await db.select() + * .from(users) + * .leftJoin(pets, eq(users.id, pets.ownerId)) + * + * // Select userId and petId + * const usersIdsAndPetIds: { userId: number; petId: number | null }[] = await db.select({ + * userId: users.id, + * petId: pets.id, + * }) + * .from(users) + * .leftJoin(pets, eq(users.id, pets.ownerId)) + * + * // Select userId and petId with use index hint + * const usersIdsAndPetIds: { userId: number; petId: number | null }[] = await db.select({ + * userId: users.id, + * petId: pets.id, + * }) + * .from(users) + * .leftJoin(pets, eq(users.id, pets.ownerId), { + * useIndex: ['pets_owner_id_index'] + * }) + * ``` + */ + leftJoin = this.createJoin('left'); + + /** + * Executes a `right join` operation by adding another table to the current query. + * + * Calling this method associates each row of the joined table with the corresponding row from the main table, if a match is found. If no matching row exists, it sets all columns of the main table to null. + * + * See docs: {@link https://orm.drizzle.team/docs/joins#right-join} + * + * @param table the table to join. + * @param on the `on` clause. + * + * @example + * + * ```ts + * // Select all users and their pets + * const usersWithPets: { user: User | null; pets: Pet }[] = await db.select() + * .from(users) + * .rightJoin(pets, eq(users.id, pets.ownerId)) + * + * // Select userId and petId + * const usersIdsAndPetIds: { userId: number | null; petId: number }[] = await db.select({ + * userId: users.id, + * petId: pets.id, + * }) + * .from(users) + * .rightJoin(pets, eq(users.id, pets.ownerId)) + * + * // Select userId and petId with use index hint + * const usersIdsAndPetIds: { userId: number; petId: number | null }[] = await db.select({ + * userId: users.id, + * petId: pets.id, + * }) + * .from(users) + * .leftJoin(pets, eq(users.id, pets.ownerId), { + * useIndex: ['pets_owner_id_index'] + * }) + * ``` + */ + rightJoin = this.createJoin('right'); + + /** + * Executes an `inner join` operation, creating a new table by combining rows from two tables that have matching values. + * + * Calling this method retrieves rows that have corresponding entries in both joined tables. Rows without matching entries in either table are excluded, resulting in a table that includes only matching pairs. + * + * See docs: {@link https://orm.drizzle.team/docs/joins#inner-join} + * + * @param table the table to join. + * @param on the `on` clause. + * + * @example + * + * ```ts + * // Select all users and their pets + * const usersWithPets: { user: User; pets: Pet }[] = await db.select() + * .from(users) + * .innerJoin(pets, eq(users.id, pets.ownerId)) + * + * // Select userId and petId + * const usersIdsAndPetIds: { userId: number; petId: number }[] = await db.select({ + * userId: users.id, + * petId: pets.id, + * }) + * .from(users) + * .innerJoin(pets, eq(users.id, pets.ownerId)) + * + * // Select userId and petId with use index hint + * const usersIdsAndPetIds: { userId: number; petId: number | null }[] = await db.select({ + * userId: users.id, + * petId: pets.id, + * }) + * .from(users) + * .leftJoin(pets, eq(users.id, pets.ownerId), { + * useIndex: ['pets_owner_id_index'] + * }) + * ``` + */ + innerJoin = this.createJoin('inner'); + + /** + * Executes a `full join` operation by combining rows from two tables into a new table. + * + * Calling this method retrieves all rows from both main and joined tables, merging rows with matching values and filling in `null` for non-matching columns. + * + * See docs: {@link https://orm.drizzle.team/docs/joins#full-join} + * + * @param table the table to join. + * @param on the `on` clause. + * + * @example + * + * ```ts + * // Select all users and their pets + * const usersWithPets: { user: User | null; pets: Pet | null }[] = await db.select() + * .from(users) + * .fullJoin(pets, eq(users.id, pets.ownerId)) + * + * // Select userId and petId + * const usersIdsAndPetIds: { userId: number | null; petId: number | null }[] = await db.select({ + * userId: users.id, + * petId: pets.id, + * }) + * .from(users) + * .fullJoin(pets, eq(users.id, pets.ownerId)) + * + * // Select userId and petId with use index hint + * const usersIdsAndPetIds: { userId: number; petId: number | null }[] = await db.select({ + * userId: users.id, + * petId: pets.id, + * }) + * .from(users) + * .leftJoin(pets, eq(users.id, pets.ownerId), { + * useIndex: ['pets_owner_id_index'] + * }) + * ``` + */ + fullJoin = this.createJoin('full'); + + private createSetOperator( + type: SetOperator, + isAll: boolean, + ): >( + rightSelection: + | ((setOperators: GetGoogleSqlSetOperators) => SetOperatorRightSelect) + | SetOperatorRightSelect, + ) => GoogleSqlSelectWithout< + this, + TDynamic, + GoogleSqlSetOperatorExcludedMethods, + true + > { + return (rightSelection) => { + const rightSelect = (typeof rightSelection === 'function' + ? rightSelection(getGoogleSqlSetOperators()) + : rightSelection) as TypedQueryBuilder< + any, + TResult + >; + + if (!haveSameKeys(this.getSelectedFields(), rightSelect.getSelectedFields())) { + throw new Error( + 'Set operator error (union / intersect / except): selected fields are not the same or are in a different order', + ); + } + + this.config.setOperators.push({ type, isAll, rightSelect }); + return this as any; + }; + } + + /** + * Adds `union` set operator to the query. + * + * Calling this method will combine the result sets of the `select` statements and remove any duplicate rows that appear across them. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#union} + * + * @example + * + * ```ts + * // Select all unique names from customers and users tables + * await db.select({ name: users.name }) + * .from(users) + * .union( + * db.select({ name: customers.name }).from(customers) + * ); + * // or + * import { union } from 'drizzle-orm/googlesql' + * + * await union( + * db.select({ name: users.name }).from(users), + * db.select({ name: customers.name }).from(customers) + * ); + * ``` + */ + union = this.createSetOperator('union', false); + + /** + * Adds `union all` set operator to the query. + * + * Calling this method will combine the result-set of the `select` statements and keep all duplicate rows that appear across them. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#union-all} + * + * @example + * + * ```ts + * // Select all transaction ids from both online and in-store sales + * await db.select({ transaction: onlineSales.transactionId }) + * .from(onlineSales) + * .unionAll( + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * // or + * import { unionAll } from 'drizzle-orm/googlesql' + * + * await unionAll( + * db.select({ transaction: onlineSales.transactionId }).from(onlineSales), + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * ``` + */ + unionAll = this.createSetOperator('union', true); + + /** + * Adds `intersect` set operator to the query. + * + * Calling this method will retain only the rows that are present in both result sets and eliminate duplicates. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect} + * + * @example + * + * ```ts + * // Select course names that are offered in both departments A and B + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .intersect( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * import { intersect } from 'drizzle-orm/googlesql' + * + * await intersect( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ + intersect = this.createSetOperator('intersect', false); + + /** + * Adds `intersect all` set operator to the query. + * + * Calling this method will retain only the rows that are present in both result sets including all duplicates. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect-all} + * + * @example + * + * ```ts + * // Select all products and quantities that are ordered by both regular and VIP customers + * await db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders) + * .intersectAll( + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * // or + * import { intersectAll } from 'drizzle-orm/googlesql' + * + * await intersectAll( + * db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders), + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * ``` + */ + intersectAll = this.createSetOperator('intersect', true); + + /** + * Adds `except` set operator to the query. + * + * Calling this method will retrieve all unique rows from the left query, except for the rows that are present in the result set of the right query. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#except} + * + * @example + * + * ```ts + * // Select all courses offered in department A but not in department B + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .except( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * import { except } from 'drizzle-orm/googlesql' + * + * await except( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ + except = this.createSetOperator('except', false); + + /** + * Adds `except all` set operator to the query. + * + * Calling this method will retrieve all rows from the left query, except for the rows that are present in the result set of the right query. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#except-all} + * + * @example + * + * ```ts + * // Select all products that are ordered by regular customers but not by VIP customers + * await db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered, + * }) + * .from(regularCustomerOrders) + * .exceptAll( + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered, + * }) + * .from(vipCustomerOrders) + * ); + * // or + * import { exceptAll } from 'drizzle-orm/googlesql' + * + * await exceptAll( + * db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders), + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * ``` + */ + exceptAll = this.createSetOperator('except', true); + + /** @internal */ + addSetOperators(setOperators: GoogleSqlSelectConfig['setOperators']): GoogleSqlSelectWithout< + this, + TDynamic, + GoogleSqlSetOperatorExcludedMethods, + true + > { + this.config.setOperators.push(...setOperators); + return this as any; + } + + /** + * Adds a `where` clause to the query. + * + * Calling this method will select only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/select#filtering} + * + * @param where the `where` clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be selected. + * + * ```ts + * // Select all cars with green color + * await db.select().from(cars).where(eq(cars.color, 'green')); + * // or + * await db.select().from(cars).where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Select all BMW cars with a green color + * await db.select().from(cars).where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Select all cars with the green or blue color + * await db.select().from(cars).where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ + where( + where: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, + ): GoogleSqlSelectWithout { + if (typeof where === 'function') { + where = where( + new Proxy( + this.config.fields, + new SelectionProxyHandler({ sqlAliasedBehavior: 'sql', sqlBehavior: 'sql' }), + ) as TSelection, + ); + } + this.config.where = where; + return this as any; + } + + /** + * Adds a `having` clause to the query. + * + * Calling this method will select only those rows that fulfill a specified condition. It is typically used with aggregate functions to filter the aggregated data based on a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/select#aggregations} + * + * @param having the `having` clause. + * + * @example + * + * ```ts + * // Select all brands with more than one car + * await db.select({ + * brand: cars.brand, + * count: sql`cast(count(${cars.id}) as int)`, + * }) + * .from(cars) + * .groupBy(cars.brand) + * .having(({ count }) => gt(count, 1)); + * ``` + */ + having( + having: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, + ): GoogleSqlSelectWithout { + if (typeof having === 'function') { + having = having( + new Proxy( + this.config.fields, + new SelectionProxyHandler({ sqlAliasedBehavior: 'sql', sqlBehavior: 'sql' }), + ) as TSelection, + ); + } + this.config.having = having; + return this as any; + } + + /** + * Adds a `group by` clause to the query. + * + * Calling this method will group rows that have the same values into summary rows, often used for aggregation purposes. + * + * See docs: {@link https://orm.drizzle.team/docs/select#aggregations} + * + * @example + * + * ```ts + * // Group and count people by their last names + * await db.select({ + * lastName: people.lastName, + * count: sql`cast(count(*) as int)` + * }) + * .from(people) + * .groupBy(people.lastName); + * ``` + */ + groupBy( + builder: (aliases: this['_']['selection']) => ValueOrArray, + ): GoogleSqlSelectWithout; + groupBy(...columns: (GoogleSqlColumn | SQL | SQL.Aliased)[]): GoogleSqlSelectWithout; + groupBy( + ...columns: + | [(aliases: this['_']['selection']) => ValueOrArray] + | (GoogleSqlColumn | SQL | SQL.Aliased)[] + ): GoogleSqlSelectWithout { + if (typeof columns[0] === 'function') { + const groupBy = columns[0]( + new Proxy( + this.config.fields, + new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + ) as TSelection, + ); + this.config.groupBy = Array.isArray(groupBy) ? groupBy : [groupBy]; + } else { + this.config.groupBy = columns as (GoogleSqlColumn | SQL | SQL.Aliased)[]; + } + return this as any; + } + + /** + * Adds an `order by` clause to the query. + * + * Calling this method will sort the result-set in ascending or descending order. By default, the sort order is ascending. + * + * See docs: {@link https://orm.drizzle.team/docs/select#order-by} + * + * @example + * + * ``` + * // Select cars ordered by year + * await db.select().from(cars).orderBy(cars.year); + * ``` + * + * You can specify whether results are in ascending or descending order with the `asc()` and `desc()` operators. + * + * ```ts + * // Select cars ordered by year in descending order + * await db.select().from(cars).orderBy(desc(cars.year)); + * + * // Select cars ordered by year and price + * await db.select().from(cars).orderBy(asc(cars.year), desc(cars.price)); + * ``` + */ + orderBy( + builder: (aliases: this['_']['selection']) => ValueOrArray, + ): GoogleSqlSelectWithout; + orderBy(...columns: (GoogleSqlColumn | SQL | SQL.Aliased)[]): GoogleSqlSelectWithout; + orderBy( + ...columns: + | [(aliases: this['_']['selection']) => ValueOrArray] + | (GoogleSqlColumn | SQL | SQL.Aliased)[] + ): GoogleSqlSelectWithout { + if (typeof columns[0] === 'function') { + const orderBy = columns[0]( + new Proxy( + this.config.fields, + new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + ) as TSelection, + ); + + const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy]; + + if (this.config.setOperators.length > 0) { + this.config.setOperators.at(-1)!.orderBy = orderByArray; + } else { + this.config.orderBy = orderByArray; + } + } else { + const orderByArray = columns as (GoogleSqlColumn | SQL | SQL.Aliased)[]; + + if (this.config.setOperators.length > 0) { + this.config.setOperators.at(-1)!.orderBy = orderByArray; + } else { + this.config.orderBy = orderByArray; + } + } + return this as any; + } + + /** + * Adds a `limit` clause to the query. + * + * Calling this method will set the maximum number of rows that will be returned by this query. + * + * See docs: {@link https://orm.drizzle.team/docs/select#limit--offset} + * + * @param limit the `limit` clause. + * + * @example + * + * ```ts + * // Get the first 10 people from this query. + * await db.select().from(people).limit(10); + * ``` + */ + limit(limit: number | Placeholder): GoogleSqlSelectWithout { + if (this.config.setOperators.length > 0) { + this.config.setOperators.at(-1)!.limit = limit; + } else { + this.config.limit = limit; + } + return this as any; + } + + /** + * Adds an `offset` clause to the query. + * + * Calling this method will skip a number of rows when returning results from this query. + * + * See docs: {@link https://orm.drizzle.team/docs/select#limit--offset} + * + * @param offset the `offset` clause. + * + * @example + * + * ```ts + * // Get the 10th-20th people from this query. + * await db.select().from(people).offset(10).limit(10); + * ``` + */ + offset(offset: number | Placeholder): GoogleSqlSelectWithout { + if (this.config.setOperators.length > 0) { + this.config.setOperators.at(-1)!.offset = offset; + } else { + this.config.offset = offset; + } + return this as any; + } + + /** + * Adds a `for` clause to the query. + * + * Calling this method will specify a lock strength for this query that controls how strictly it acquires exclusive access to the rows being queried. + * + * See docs: {@link https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html} + * + * @param strength the lock strength. + * @param config the lock configuration. + */ + for(strength: LockStrength, config: LockConfig = {}): GoogleSqlSelectWithout { + this.config.lockingClause = { strength, config }; + return this as any; + } + + /** @internal */ + getSQL(): SQL { + return this.dialect.buildSelectQuery(this.config); + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + as( + alias: TAlias, + ): SubqueryWithSelection { + return new Proxy( + new Subquery(this.getSQL(), this.config.fields, alias), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), + ) as SubqueryWithSelection; + } + + /** @internal */ + override getSelectedFields(): this['_']['selectedFields'] { + return new Proxy( + this.config.fields, + new SelectionProxyHandler({ alias: this.tableName, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), + ) as this['_']['selectedFields']; + } + + $dynamic(): GoogleSqlSelectDynamic { + return this as any; + } +} + +export interface GoogleSqlSelectBase< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends + GoogleSqlSelectQueryBuilderBase< + GoogleSqlSelectHKT, + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + QueryPromise +{} + +export class GoogleSqlSelectBase< + TTableName extends string | undefined, + TSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields = BuildSubquerySelection, +> extends GoogleSqlSelectQueryBuilderBase< + GoogleSqlSelectHKT, + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields +> { + static override readonly [entityKind]: string = 'GoogleSqlSelect'; + + prepare(): GoogleSqlSelectPrepare { + if (!this.session) { + throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); + } + const fieldsList = orderSelectedFields(this.config.fields); + const query = this.session.prepareQuery< + GoogleSqlPreparedQueryConfig & { execute: SelectResult[] }, + TPreparedQueryHKT + >(this.dialect.sqlToQuery(this.getSQL()), fieldsList); + query.joinsNotNullableMap = this.joinsNotNullableMap; + return query as GoogleSqlSelectPrepare; + } + + execute = ((placeholderValues) => { + return this.prepare().execute(placeholderValues); + }) as ReturnType['execute']; + + private createIterator = (): ReturnType['iterator'] => { + const self = this; + return async function*(placeholderValues) { + yield* self.prepare().iterator(placeholderValues); + }; + }; + + iterator = this.createIterator(); +} + +applyMixins(GoogleSqlSelectBase, [QueryPromise]); + +function createSetOperator(type: SetOperator, isAll: boolean): GoogleSqlCreateSetOperatorFn { + return (leftSelect, rightSelect, ...restSelects) => { + const setOperators = [rightSelect, ...restSelects].map((select) => ({ + type, + isAll, + rightSelect: select as AnyGoogleSqlSelect, + })); + + for (const setOperator of setOperators) { + if (!haveSameKeys((leftSelect as any).getSelectedFields(), setOperator.rightSelect.getSelectedFields())) { + throw new Error( + 'Set operator error (union / intersect / except): selected fields are not the same or are in a different order', + ); + } + } + + return (leftSelect as AnyGoogleSqlSelect).addSetOperators(setOperators) as any; + }; +} + +const getGoogleSqlSetOperators = () => ({ + union, + unionAll, + intersect, + intersectAll, + except, + exceptAll, +}); + +/** + * Adds `union` set operator to the query. + * + * Calling this method will combine the result sets of the `select` statements and remove any duplicate rows that appear across them. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#union} + * + * @example + * + * ```ts + * // Select all unique names from customers and users tables + * import { union } from 'drizzle-orm/googlesql' + * + * await union( + * db.select({ name: users.name }).from(users), + * db.select({ name: customers.name }).from(customers) + * ); + * // or + * await db.select({ name: users.name }) + * .from(users) + * .union( + * db.select({ name: customers.name }).from(customers) + * ); + * ``` + */ +export const union = createSetOperator('union', false); + +/** + * Adds `union all` set operator to the query. + * + * Calling this method will combine the result-set of the `select` statements and keep all duplicate rows that appear across them. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#union-all} + * + * @example + * + * ```ts + * // Select all transaction ids from both online and in-store sales + * import { unionAll } from 'drizzle-orm/googlesql' + * + * await unionAll( + * db.select({ transaction: onlineSales.transactionId }).from(onlineSales), + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * // or + * await db.select({ transaction: onlineSales.transactionId }) + * .from(onlineSales) + * .unionAll( + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * ``` + */ +export const unionAll = createSetOperator('union', true); + +/** + * Adds `intersect` set operator to the query. + * + * Calling this method will retain only the rows that are present in both result sets and eliminate duplicates. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect} + * + * @example + * + * ```ts + * // Select course names that are offered in both departments A and B + * import { intersect } from 'drizzle-orm/googlesql' + * + * await intersect( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .intersect( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ +export const intersect = createSetOperator('intersect', false); + +/** + * Adds `intersect all` set operator to the query. + * + * Calling this method will retain only the rows that are present in both result sets including all duplicates. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect-all} + * + * @example + * + * ```ts + * // Select all products and quantities that are ordered by both regular and VIP customers + * import { intersectAll } from 'drizzle-orm/googlesql' + * + * await intersectAll( + * db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders), + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * // or + * await db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders) + * .intersectAll( + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * ``` + */ +export const intersectAll = createSetOperator('intersect', true); + +/** + * Adds `except` set operator to the query. + * + * Calling this method will retrieve all unique rows from the left query, except for the rows that are present in the result set of the right query. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#except} + * + * @example + * + * ```ts + * // Select all courses offered in department A but not in department B + * import { except } from 'drizzle-orm/googlesql' + * + * await except( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .except( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ +export const except = createSetOperator('except', false); + +/** + * Adds `except all` set operator to the query. + * + * Calling this method will retrieve all rows from the left query, except for the rows that are present in the result set of the right query. + * + * See docs: {@link https://orm.drizzle.team/docs/set-operations#except-all} + * + * @example + * + * ```ts + * // Select all products that are ordered by regular customers but not by VIP customers + * import { exceptAll } from 'drizzle-orm/googlesql' + * + * await exceptAll( + * db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders), + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * // or + * await db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered, + * }) + * .from(regularCustomerOrders) + * .exceptAll( + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered, + * }) + * .from(vipCustomerOrders) + * ); + * ``` + */ +export const exceptAll = createSetOperator('except', true); diff --git a/drizzle-orm/src/googlesql/query-builders/select.types.ts b/drizzle-orm/src/googlesql/query-builders/select.types.ts new file mode 100644 index 0000000000..b25c7213c1 --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/select.types.ts @@ -0,0 +1,466 @@ +import type { GoogleSqlColumn } from '~/googlesql/columns/index.ts'; +import type { GoogleSqlTable, GoogleSqlTableWithColumns } from '~/googlesql/table.ts'; +import type { + SelectedFields as SelectedFieldsBase, + SelectedFieldsFlat as SelectedFieldsFlatBase, + SelectedFieldsOrdered as SelectedFieldsOrderedBase, +} from '~/operations.ts'; +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { + AppendToNullabilityMap, + AppendToResult, + BuildSubquerySelection, + GetSelectTableName, + JoinNullability, + JoinType, + MapColumnsToTableAlias, + SelectMode, + SelectResult, + SetOperator, +} from '~/query-builders/select.types.ts'; +import type { ColumnsSelection, Placeholder, SQL, View } from '~/sql/sql.ts'; +import type { Subquery } from '~/subquery.ts'; +import type { Table, UpdateTableConfig } from '~/table.ts'; +import type { Assume, ValidateShape } from '~/utils.ts'; +import type { GoogleSqlPreparedQueryConfig, PreparedQueryHKTBase, PreparedQueryKind } from '../session.ts'; +import type { GoogleSqlViewBase } from '../view-base.ts'; +import type { GoogleSqlViewWithSelection } from '../view.ts'; +import type { GoogleSqlSelectBase, GoogleSqlSelectQueryBuilderBase, IndexConfig } from './select.ts'; + +export interface GoogleSqlSelectJoinConfig { + on: SQL | undefined; + table: GoogleSqlTable | Subquery | GoogleSqlViewBase | SQL; + alias: string | undefined; + joinType: JoinType; + lateral?: boolean; + useIndex?: string[]; + forceIndex?: string[]; + ignoreIndex?: string[]; +} + +export type BuildAliasTable = TTable extends Table + ? GoogleSqlTableWithColumns< + UpdateTableConfig; + }> + > + : TTable extends View ? GoogleSqlViewWithSelection< + TAlias, + TTable['_']['existing'], + MapColumnsToTableAlias + > + : never; + +export interface GoogleSqlSelectConfig { + withList?: Subquery[]; + fields: Record; + fieldsFlat?: SelectedFieldsOrdered; + where?: SQL; + having?: SQL; + table: GoogleSqlTable | Subquery | GoogleSqlViewBase | SQL; + limit?: number | Placeholder; + offset?: number | Placeholder; + joins?: GoogleSqlSelectJoinConfig[]; + orderBy?: (GoogleSqlColumn | SQL | SQL.Aliased)[]; + groupBy?: (GoogleSqlColumn | SQL | SQL.Aliased)[]; + lockingClause?: { + strength: LockStrength; + config: LockConfig; + }; + distinct?: boolean; + setOperators: { + rightSelect: TypedQueryBuilder; + type: SetOperator; + isAll: boolean; + orderBy?: (GoogleSqlColumn | SQL | SQL.Aliased)[]; + limit?: number | Placeholder; + offset?: number | Placeholder; + }[]; + useIndex?: string[]; + forceIndex?: string[]; + ignoreIndex?: string[]; +} + +export type GoogleSqlJoin< + T extends AnyGoogleSqlSelectQueryBuilder, + TDynamic extends boolean, + TJoinType extends JoinType, + TJoinedTable extends GoogleSqlTable | Subquery | GoogleSqlViewBase | SQL, + TJoinedName extends GetSelectTableName = GetSelectTableName, +> = T extends any ? GoogleSqlSelectWithout< + GoogleSqlSelectKind< + T['_']['hkt'], + T['_']['tableName'], + AppendToResult< + T['_']['tableName'], + T['_']['selection'], + TJoinedName, + TJoinedTable extends GoogleSqlTable ? TJoinedTable['_']['columns'] + : TJoinedTable extends Subquery | View ? Assume + : never, + T['_']['selectMode'] + >, + T['_']['selectMode'] extends 'partial' ? T['_']['selectMode'] : 'multiple', + T['_']['preparedQueryHKT'], + AppendToNullabilityMap, + TDynamic, + T['_']['excludedMethods'] + >, + TDynamic, + T['_']['excludedMethods'] + > + : never; + +export type GoogleSqlJoinFn< + T extends AnyGoogleSqlSelectQueryBuilder, + TDynamic extends boolean, + TJoinType extends JoinType, +> = < + TJoinedTable extends GoogleSqlTable | Subquery | GoogleSqlViewBase | SQL, + TJoinedName extends GetSelectTableName = GetSelectTableName, +>( + table: TJoinedTable, + on: ((aliases: T['_']['selection']) => SQL | undefined) | SQL | undefined, + onIndex?: TJoinedTable extends GoogleSqlTable ? IndexConfig + : 'Index hint configuration is allowed only for GoogleSqlTable and not for subqueries or views', +) => GoogleSqlJoin; + +export type SelectedFieldsFlat = SelectedFieldsFlatBase; + +export type SelectedFields = SelectedFieldsBase; + +export type SelectedFieldsOrdered = SelectedFieldsOrderedBase; + +export type LockStrength = 'update' | 'share'; + +export type LockConfig = { + noWait: true; + skipLocked?: undefined; +} | { + noWait?: undefined; + skipLocked: true; +} | { + noWait?: undefined; + skipLocked?: undefined; +}; + +export interface GoogleSqlSelectHKTBase { + tableName: string | undefined; + selection: unknown; + selectMode: SelectMode; + preparedQueryHKT: unknown; + nullabilityMap: unknown; + dynamic: boolean; + excludedMethods: string; + result: unknown; + selectedFields: unknown; + _type: unknown; +} + +export type GoogleSqlSelectKind< + T extends GoogleSqlSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record, + TDynamic extends boolean, + TExcludedMethods extends string, + TResult = SelectResult[], + TSelectedFields = BuildSubquerySelection, +> = (T & { + tableName: TTableName; + selection: TSelection; + selectMode: TSelectMode; + preparedQueryHKT: TPreparedQueryHKT; + nullabilityMap: TNullabilityMap; + dynamic: TDynamic; + excludedMethods: TExcludedMethods; + result: TResult; + selectedFields: TSelectedFields; +})['_type']; + +export interface GoogleSqlSelectQueryBuilderHKT extends GoogleSqlSelectHKTBase { + _type: GoogleSqlSelectQueryBuilderBase< + GoogleSqlSelectQueryBuilderHKT, + this['tableName'], + Assume, + this['selectMode'], + Assume, + Assume>, + this['dynamic'], + this['excludedMethods'], + Assume, + Assume + >; +} + +export interface GoogleSqlSelectHKT extends GoogleSqlSelectHKTBase { + _type: GoogleSqlSelectBase< + this['tableName'], + Assume, + this['selectMode'], + Assume, + Assume>, + this['dynamic'], + this['excludedMethods'], + Assume, + Assume + >; +} + +export type GoogleSqlSetOperatorExcludedMethods = + | 'where' + | 'having' + | 'groupBy' + | 'session' + | 'leftJoin' + | 'rightJoin' + | 'innerJoin' + | 'fullJoin' + | 'for'; + +export type GoogleSqlSelectWithout< + T extends AnyGoogleSqlSelectQueryBuilder, + TDynamic extends boolean, + K extends keyof T & string, + TResetExcluded extends boolean = false, +> = TDynamic extends true ? T : Omit< + GoogleSqlSelectKind< + T['_']['hkt'], + T['_']['tableName'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['preparedQueryHKT'], + T['_']['nullabilityMap'], + TDynamic, + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K, + T['_']['result'], + T['_']['selectedFields'] + >, + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K +>; + +export type GoogleSqlSelectPrepare = PreparedQueryKind< + T['_']['preparedQueryHKT'], + GoogleSqlPreparedQueryConfig & { + execute: T['_']['result']; + iterator: T['_']['result'][number]; + }, + true +>; + +export type GoogleSqlSelectDynamic = GoogleSqlSelectKind< + T['_']['hkt'], + T['_']['tableName'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['preparedQueryHKT'], + T['_']['nullabilityMap'], + true, + never, + T['_']['result'], + T['_']['selectedFields'] +>; + +export type CreateGoogleSqlSelectFromBuilderMode< + TBuilderMode extends 'db' | 'qb', + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, +> = TBuilderMode extends 'db' ? GoogleSqlSelectBase + : GoogleSqlSelectQueryBuilderBase< + GoogleSqlSelectQueryBuilderHKT, + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT + >; + +export type GoogleSqlSelectQueryBuilder< + THKT extends GoogleSqlSelectHKTBase = GoogleSqlSelectQueryBuilderHKT, + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = ColumnsSelection, + TSelectMode extends SelectMode = SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TNullabilityMap extends Record = Record, + TResult extends any[] = unknown[], + TSelectedFields extends ColumnsSelection = ColumnsSelection, +> = GoogleSqlSelectQueryBuilderBase< + THKT, + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + true, + never, + TResult, + TSelectedFields +>; + +export type AnyGoogleSqlSelectQueryBuilder = GoogleSqlSelectQueryBuilderBase< + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type AnyGoogleSqlSetOperatorInterface = GoogleSqlSetOperatorInterface< + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export interface GoogleSqlSetOperatorInterface< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> { + _: { + readonly hkt: GoogleSqlSelectHKT; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; +} + +export type GoogleSqlSetOperatorWithResult = GoogleSqlSetOperatorInterface< + any, + any, + any, + any, + any, + any, + any, + TResult, + any +>; + +export type GoogleSqlSelect< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = GoogleSqlSelectBase; + +export type AnyGoogleSqlSelect = GoogleSqlSelectBase; + +export type GoogleSqlSetOperator< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TNullabilityMap extends Record = Record, +> = GoogleSqlSelectBase< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + true, + GoogleSqlSetOperatorExcludedMethods +>; + +export type SetOperatorRightSelect< + TValue extends GoogleSqlSetOperatorWithResult, + TResult extends any[], +> = TValue extends GoogleSqlSetOperatorInterface + ? ValidateShape< + TValueResult[number], + TResult[number], + TypedQueryBuilder + > + : TValue; + +export type SetOperatorRestSelect< + TValue extends readonly GoogleSqlSetOperatorWithResult[], + TResult extends any[], +> = TValue extends [infer First, ...infer Rest] + ? First extends GoogleSqlSetOperatorInterface + ? Rest extends AnyGoogleSqlSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> + : never + : TValue; + +export type GoogleSqlCreateSetOperatorFn = < + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TValue extends GoogleSqlSetOperatorWithResult, + TRest extends GoogleSqlSetOperatorWithResult[], + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +>( + leftSelect: GoogleSqlSetOperatorInterface< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect +) => GoogleSqlSelectWithout< + GoogleSqlSelectBase< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + false, + GoogleSqlSetOperatorExcludedMethods, + true +>; + +export type GetGoogleSqlSetOperators = { + union: GoogleSqlCreateSetOperatorFn; + intersect: GoogleSqlCreateSetOperatorFn; + except: GoogleSqlCreateSetOperatorFn; + unionAll: GoogleSqlCreateSetOperatorFn; + intersectAll: GoogleSqlCreateSetOperatorFn; + exceptAll: GoogleSqlCreateSetOperatorFn; +}; diff --git a/drizzle-orm/src/googlesql/query-builders/update.ts b/drizzle-orm/src/googlesql/query-builders/update.ts new file mode 100644 index 0000000000..7dce6470c6 --- /dev/null +++ b/drizzle-orm/src/googlesql/query-builders/update.ts @@ -0,0 +1,252 @@ +import type { GetColumnData } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { GoogleSqlDialect } from '~/googlesql/dialect.ts'; +import type { + AnyGoogleSqlQueryResultHKT, + GoogleSqlPreparedQueryConfig, + GoogleSqlQueryResultHKT, + GoogleSqlQueryResultKind, + GoogleSqlSession, + PreparedQueryHKTBase, + PreparedQueryKind, +} from '~/googlesql/session.ts'; +import type { GoogleSqlTable } from '~/googlesql/table.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; +import type { Placeholder, Query, SQL, SQLWrapper } from '~/sql/sql.ts'; +import type { Subquery } from '~/subquery.ts'; +import { Table } from '~/table.ts'; +import { mapUpdateSet, type UpdateSet, type ValueOrArray } from '~/utils.ts'; +import type { GoogleSqlColumn } from '../columns/common.ts'; +import type { SelectedFieldsOrdered } from './select.types.ts'; + +export interface GoogleSqlUpdateConfig { + where?: SQL | undefined; + limit?: number | Placeholder; + orderBy?: (GoogleSqlColumn | SQL | SQL.Aliased)[]; + set: UpdateSet; + table: GoogleSqlTable; + returning?: SelectedFieldsOrdered; + withList?: Subquery[]; +} + +export type GoogleSqlUpdateSetSource = + & { + [Key in keyof TTable['$inferInsert']]?: + | GetColumnData + | SQL + | undefined; + } + & {}; + +export class GoogleSqlUpdateBuilder< + TTable extends GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, +> { + static readonly [entityKind]: string = 'GoogleSqlUpdateBuilder'; + + declare readonly _: { + readonly table: TTable; + }; + + constructor( + private table: TTable, + private session: GoogleSqlSession, + private dialect: GoogleSqlDialect, + private withList?: Subquery[], + ) {} + + set(values: GoogleSqlUpdateSetSource): GoogleSqlUpdateBase { + return new GoogleSqlUpdateBase( + this.table, + mapUpdateSet(this.table, values), + this.session, + this.dialect, + this.withList, + ); + } +} + +export type GoogleSqlUpdateWithout< + T extends AnyGoogleSqlUpdateBase, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true ? T : Omit< + GoogleSqlUpdateBase< + T['_']['table'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'], + TDynamic, + T['_']['excludedMethods'] | K + >, + T['_']['excludedMethods'] | K +>; + +export type GoogleSqlUpdatePrepare = PreparedQueryKind< + T['_']['preparedQueryHKT'], + GoogleSqlPreparedQueryConfig & { + execute: GoogleSqlQueryResultKind; + iterator: never; + }, + true +>; + +export type GoogleSqlUpdateDynamic = GoogleSqlUpdate< + T['_']['table'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'] +>; + +export type GoogleSqlUpdate< + TTable extends GoogleSqlTable = GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT = AnyGoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, +> = GoogleSqlUpdateBase; + +export type AnyGoogleSqlUpdateBase = GoogleSqlUpdateBase; + +export interface GoogleSqlUpdateBase< + TTable extends GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, +> extends QueryPromise>, SQLWrapper { + readonly _: { + readonly table: TTable; + readonly queryResult: TQueryResult; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + }; +} + +export class GoogleSqlUpdateBase< + TTable extends GoogleSqlTable, + TQueryResult extends GoogleSqlQueryResultHKT, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TPreparedQueryHKT extends PreparedQueryHKTBase, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TDynamic extends boolean = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TExcludedMethods extends string = never, +> extends QueryPromise> implements SQLWrapper { + static override readonly [entityKind]: string = 'GoogleSqlUpdate'; + + private config: GoogleSqlUpdateConfig; + + constructor( + table: TTable, + set: UpdateSet, + private session: GoogleSqlSession, + private dialect: GoogleSqlDialect, + withList?: Subquery[], + ) { + super(); + this.config = { set, table, withList }; + } + + /** + * Adds a 'where' clause to the query. + * + * Calling this method will update only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param where the 'where' clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be updated. + * + * ```ts + * // Update all cars with green color + * db.update(cars).set({ color: 'red' }) + * .where(eq(cars.color, 'green')); + * // or + * db.update(cars).set({ color: 'red' }) + * .where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Update all BMW cars with a green color + * db.update(cars).set({ color: 'red' }) + * .where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Update all cars with the green or blue color + * db.update(cars).set({ color: 'red' }) + * .where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ + where(where: SQL | undefined): GoogleSqlUpdateWithout { + this.config.where = where; + return this as any; + } + + orderBy( + builder: (updateTable: TTable) => ValueOrArray, + ): GoogleSqlUpdateWithout; + orderBy(...columns: (GoogleSqlColumn | SQL | SQL.Aliased)[]): GoogleSqlUpdateWithout; + orderBy( + ...columns: + | [(updateTable: TTable) => ValueOrArray] + | (GoogleSqlColumn | SQL | SQL.Aliased)[] + ): GoogleSqlUpdateWithout { + if (typeof columns[0] === 'function') { + const orderBy = columns[0]( + new Proxy( + this.config.table[Table.Symbol.Columns], + new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + ) as any, + ); + + const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy]; + this.config.orderBy = orderByArray; + } else { + const orderByArray = columns as (GoogleSqlColumn | SQL | SQL.Aliased)[]; + this.config.orderBy = orderByArray; + } + return this as any; + } + + limit(limit: number | Placeholder): GoogleSqlUpdateWithout { + this.config.limit = limit; + return this as any; + } + + /** @internal */ + getSQL(): SQL { + return this.dialect.buildUpdateQuery(this.config); + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + prepare(): GoogleSqlUpdatePrepare { + return this.session.prepareQuery( + this.dialect.sqlToQuery(this.getSQL()), + this.config.returning, + ) as GoogleSqlUpdatePrepare; + } + + override execute: ReturnType['execute'] = (placeholderValues) => { + return this.prepare().execute(placeholderValues); + }; + + private createIterator = (): ReturnType['iterator'] => { + const self = this; + return async function*(placeholderValues) { + yield* self.prepare().iterator(placeholderValues); + }; + }; + + iterator = this.createIterator(); + + $dynamic(): GoogleSqlUpdateDynamic { + return this as any; + } +} diff --git a/drizzle-orm/src/googlesql/schema.ts b/drizzle-orm/src/googlesql/schema.ts new file mode 100644 index 0000000000..6104a2d91d --- /dev/null +++ b/drizzle-orm/src/googlesql/schema.ts @@ -0,0 +1,40 @@ +import { entityKind, is } from '~/entity.ts'; +import { type GoogleSqlTableFn, googlesqlTableWithSchema } from './table.ts'; +import { type googlesqlView, googlesqlViewWithSchema } from './view.ts'; + +export class GoogleSqlSchema { + static readonly [entityKind]: string = 'GoogleSqlSchema'; + + constructor( + public readonly schemaName: TName, + ) {} + + table: GoogleSqlTableFn = (name, columns, extraConfig) => { + return googlesqlTableWithSchema(name, columns, extraConfig, this.schemaName); + }; + + view = ((name, columns) => { + return googlesqlViewWithSchema(name, columns, this.schemaName); + }) as typeof googlesqlView; +} + +/** @deprecated - use `instanceof GoogleSqlSchema` */ +export function isGoogleSqlSchema(obj: unknown): obj is GoogleSqlSchema { + return is(obj, GoogleSqlSchema); +} + +/** + * Create a MySQL schema. + * https://dev.mysql.com/doc/refman/8.0/en/create-database.html + * + * @param name googlesql use schema name + * @returns MySQL schema + */ +export function googlesqlDatabase(name: TName) { + return new GoogleSqlSchema(name); +} + +/** + * @see googlesqlDatabase + */ +export const googlesqlSchema = googlesqlDatabase; diff --git a/drizzle-orm/src/googlesql/session.ts b/drizzle-orm/src/googlesql/session.ts new file mode 100644 index 0000000000..443cc651f5 --- /dev/null +++ b/drizzle-orm/src/googlesql/session.ts @@ -0,0 +1,157 @@ +import { entityKind } from '~/entity.ts'; +import { TransactionRollbackError } from '~/errors.ts'; +import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; +import { type Query, type SQL, sql } from '~/sql/sql.ts'; +import type { Assume, Equal } from '~/utils.ts'; +import { GoogleSqlDatabase } from './db.ts'; +import type { GoogleSqlDialect } from './dialect.ts'; +import type { SelectedFieldsOrdered } from './query-builders/select.types.ts'; + +export type Mode = 'default' | 'planetscale'; + +export interface GoogleSqlQueryResultHKT { + readonly $brand: 'GoogleSqlQueryResultHKT'; + readonly row: unknown; + readonly type: unknown; +} + +export interface AnyGoogleSqlQueryResultHKT extends GoogleSqlQueryResultHKT { + readonly type: any; +} + +export type GoogleSqlQueryResultKind = (TKind & { + readonly row: TRow; +})['type']; + +export interface GoogleSqlPreparedQueryConfig { + execute: unknown; + iterator: unknown; +} + +export interface GoogleSqlPreparedQueryHKT { + readonly $brand: 'GoogleSqlPreparedQueryHKT'; + readonly config: unknown; + readonly type: unknown; +} + +export type PreparedQueryKind< + TKind extends GoogleSqlPreparedQueryHKT, + TConfig extends GoogleSqlPreparedQueryConfig, + TAssume extends boolean = false, +> = Equal extends true + ? Assume<(TKind & { readonly config: TConfig })['type'], GoogleSqlPreparedQuery> + : (TKind & { readonly config: TConfig })['type']; + +export abstract class GoogleSqlPreparedQuery { + static readonly [entityKind]: string = 'GoogleSqlPreparedQuery'; + + /** @internal */ + joinsNotNullableMap?: Record; + + abstract execute(placeholderValues?: Record): Promise; + + abstract iterator(placeholderValues?: Record): AsyncGenerator; +} + +export interface GoogleSqlTransactionConfig { + withConsistentSnapshot?: boolean; + accessMode?: 'read only' | 'read write'; + isolationLevel: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'; +} + +export abstract class GoogleSqlSession< + TQueryResult extends GoogleSqlQueryResultHKT = GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TFullSchema extends Record = Record, + TSchema extends TablesRelationalConfig = Record, +> { + static readonly [entityKind]: string = 'GoogleSqlSession'; + + constructor(protected dialect: GoogleSqlDialect) {} + + abstract prepareQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + customResultMapper?: (rows: unknown[][]) => T['execute'], + generatedIds?: Record[], + returningIds?: SelectedFieldsOrdered, + ): PreparedQueryKind; + + execute(query: SQL): Promise { + return this.prepareQuery( + this.dialect.sqlToQuery(query), + undefined, + ).execute(); + } + + abstract all(query: SQL): Promise; + + async count(sql: SQL): Promise { + const res = await this.execute<[[{ count: string }]]>(sql); + + return Number( + res[0][0]['count'], + ); + } + + abstract transaction( + transaction: (tx: GoogleSqlTransaction) => Promise, + config?: GoogleSqlTransactionConfig, + ): Promise; + + protected getSetTransactionSQL(config: GoogleSqlTransactionConfig): SQL | undefined { + const parts: string[] = []; + + if (config.isolationLevel) { + parts.push(`isolation level ${config.isolationLevel}`); + } + + return parts.length ? sql`set transaction ${sql.raw(parts.join(' '))}` : undefined; + } + + protected getStartTransactionSQL(config: GoogleSqlTransactionConfig): SQL | undefined { + const parts: string[] = []; + + if (config.withConsistentSnapshot) { + parts.push('with consistent snapshot'); + } + + if (config.accessMode) { + parts.push(config.accessMode); + } + + return parts.length ? sql`start transaction ${sql.raw(parts.join(' '))}` : undefined; + } +} + +export abstract class GoogleSqlTransaction< + TQueryResult extends GoogleSqlQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TFullSchema extends Record = Record, + TSchema extends TablesRelationalConfig = Record, +> extends GoogleSqlDatabase { + static override readonly [entityKind]: string = 'GoogleSqlTransaction'; + + constructor( + dialect: GoogleSqlDialect, + session: GoogleSqlSession, + protected schema: RelationalSchemaConfig | undefined, + protected readonly nestedIndex: number, + mode: Mode, + ) { + super(dialect, session, schema, mode); + } + + rollback(): never { + throw new TransactionRollbackError(); + } + + /** Nested transactions (aka savepoints) only work with InnoDB engine. */ + abstract override transaction( + transaction: (tx: GoogleSqlTransaction) => Promise, + ): Promise; +} + +export interface PreparedQueryHKTBase extends GoogleSqlPreparedQueryHKT { + type: GoogleSqlPreparedQuery>; +} diff --git a/drizzle-orm/src/googlesql/subquery.ts b/drizzle-orm/src/googlesql/subquery.ts new file mode 100644 index 0000000000..ca2fd2f9ce --- /dev/null +++ b/drizzle-orm/src/googlesql/subquery.ts @@ -0,0 +1,35 @@ +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { AddAliasToSelection } from '~/query-builders/select.types.ts'; +import type { ColumnsSelection, SQL } from '~/sql/sql.ts'; +import type { Subquery, WithSubquery, WithSubqueryWithoutSelection } from '~/subquery.ts'; +import type { QueryBuilder } from './query-builders/query-builder.ts'; + +export type SubqueryWithSelection< + TSelection extends ColumnsSelection, + TAlias extends string, +> = + & Subquery> + & AddAliasToSelection; + +export type WithSubqueryWithSelection< + TSelection extends ColumnsSelection, + TAlias extends string, +> = + & WithSubquery> + & AddAliasToSelection; + +export interface WithBuilder { + (alias: TAlias): { + as: { + ( + qb: TypedQueryBuilder | ((qb: QueryBuilder) => TypedQueryBuilder), + ): WithSubqueryWithSelection; + ( + qb: TypedQueryBuilder | ((qb: QueryBuilder) => TypedQueryBuilder), + ): WithSubqueryWithoutSelection; + }; + }; + (alias: TAlias, selection: TSelection): { + as: (qb: SQL | ((qb: QueryBuilder) => SQL)) => WithSubqueryWithSelection; + }; +} diff --git a/drizzle-orm/src/googlesql/table.ts b/drizzle-orm/src/googlesql/table.ts new file mode 100644 index 0000000000..6e71d2be3e --- /dev/null +++ b/drizzle-orm/src/googlesql/table.ts @@ -0,0 +1,229 @@ +import type { BuildColumns, BuildExtraConfigColumns } from '~/column-builder.ts'; +import { entityKind } from '~/entity.ts'; +import { Table, type TableConfig as TableConfigBase, type UpdateTableConfig } from '~/table.ts'; +import type { CheckBuilder } from './checks.ts'; +import { getGoogleSqlColumnBuilders, type GoogleSqlColumnBuilders } from './columns/all.ts'; +import type { GoogleSqlColumn, GoogleSqlColumnBuilder, GoogleSqlColumnBuilderBase } from './columns/common.ts'; +import type { ForeignKey, ForeignKeyBuilder } from './foreign-keys.ts'; +import type { AnyIndexBuilder } from './indexes.ts'; +import type { PrimaryKeyBuilder } from './primary-keys.ts'; +import type { UniqueConstraintBuilder } from './unique-constraint.ts'; + +export type GoogleSqlTableExtraConfigValue = + | AnyIndexBuilder + | CheckBuilder + | ForeignKeyBuilder + | PrimaryKeyBuilder + | UniqueConstraintBuilder; + +export type GoogleSqlTableExtraConfig = Record< + string, + GoogleSqlTableExtraConfigValue +>; + +export type TableConfig = TableConfigBase; + +/** @internal */ +export const InlineForeignKeys = Symbol.for('drizzle:GoogleSqlInlineForeignKeys'); + +export class GoogleSqlTable extends Table { + static override readonly [entityKind]: string = 'GoogleSqlTable'; + + declare protected $columns: T['columns']; + + /** @internal */ + static override readonly Symbol = Object.assign({}, Table.Symbol, { + InlineForeignKeys: InlineForeignKeys as typeof InlineForeignKeys, + }); + + /** @internal */ + override [Table.Symbol.Columns]!: NonNullable; + + /** @internal */ + [InlineForeignKeys]: ForeignKey[] = []; + + /** @internal */ + override [Table.Symbol.ExtraConfigBuilder]: + | ((self: Record) => GoogleSqlTableExtraConfig) + | undefined = undefined; +} + +export type AnyGoogleSqlTable = {}> = GoogleSqlTable< + UpdateTableConfig +>; + +export type GoogleSqlTableWithColumns = + & GoogleSqlTable + & { + [Key in keyof T['columns']]: T['columns'][Key]; + }; + +export function googlesqlTableWithSchema< + TTableName extends string, + TSchemaName extends string | undefined, + TColumnsMap extends Record, +>( + name: TTableName, + columns: TColumnsMap | ((columnTypes: GoogleSqlColumnBuilders) => TColumnsMap), + extraConfig: + | (( + self: BuildColumns, + ) => GoogleSqlTableExtraConfig | GoogleSqlTableExtraConfigValue[]) + | undefined, + schema: TSchemaName, + baseName = name, +): GoogleSqlTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'googlesql'; +}> { + const rawTable = new GoogleSqlTable<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'googlesql'; + }>(name, schema, baseName); + + const parsedColumns: TColumnsMap = typeof columns === 'function' ? columns(getGoogleSqlColumnBuilders()) : columns; + + const builtColumns = Object.fromEntries( + Object.entries(parsedColumns).map(([name, colBuilderBase]) => { + const colBuilder = colBuilderBase as GoogleSqlColumnBuilder; + colBuilder.setName(name); + const column = colBuilder.build(rawTable); + rawTable[InlineForeignKeys].push(...colBuilder.buildForeignKeys(column, rawTable)); + return [name, column]; + }), + ) as unknown as BuildColumns; + + const table = Object.assign(rawTable, builtColumns); + + table[Table.Symbol.Columns] = builtColumns; + table[Table.Symbol.ExtraConfigColumns] = builtColumns as unknown as BuildExtraConfigColumns< + TTableName, + TColumnsMap, + 'googlesql' + >; + + if (extraConfig) { + table[GoogleSqlTable.Symbol.ExtraConfigBuilder] = extraConfig as unknown as ( + self: Record, + ) => GoogleSqlTableExtraConfig; + } + + return table; +} + +export interface GoogleSqlTableFn { + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: TColumnsMap, + extraConfig?: ( + self: BuildColumns, + ) => GoogleSqlTableExtraConfigValue[], + ): GoogleSqlTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'googlesql'; + }>; + + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: (columnTypes: GoogleSqlColumnBuilders) => TColumnsMap, + extraConfig?: (self: BuildColumns) => GoogleSqlTableExtraConfigValue[], + ): GoogleSqlTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'googlesql'; + }>; + /** + * @deprecated The third parameter of googlesqlTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = googlesqlTable("users", { + * id: int(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = googlesqlTable("users", { + * id: int(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` + */ + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: TColumnsMap, + extraConfig: (self: BuildColumns) => GoogleSqlTableExtraConfig, + ): GoogleSqlTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'googlesql'; + }>; + + /** + * @deprecated The third parameter of googlesqlTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = googlesqlTable("users", { + * id: int(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = googlesqlTable("users", { + * id: int(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` + */ + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: (columnTypes: GoogleSqlColumnBuilders) => TColumnsMap, + extraConfig: (self: BuildColumns) => GoogleSqlTableExtraConfig, + ): GoogleSqlTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'googlesql'; + }>; +} + +export const googlesqlTable: GoogleSqlTableFn = (name, columns, extraConfig) => { + return googlesqlTableWithSchema(name, columns, extraConfig, undefined, name); +}; + +export function googlesqlTableCreator(customizeTableName: (name: string) => string): GoogleSqlTableFn { + return (name, columns, extraConfig) => { + return googlesqlTableWithSchema(customizeTableName(name) as typeof name, columns, extraConfig, undefined, name); + }; +} diff --git a/drizzle-orm/src/googlesql/unique-constraint.ts b/drizzle-orm/src/googlesql/unique-constraint.ts new file mode 100644 index 0000000000..1bc72b8ce5 --- /dev/null +++ b/drizzle-orm/src/googlesql/unique-constraint.ts @@ -0,0 +1,65 @@ +import { entityKind } from '~/entity.ts'; +import { TableName } from '~/table.utils.ts'; +import type { GoogleSqlColumn } from './columns/index.ts'; +import type { GoogleSqlTable } from './table.ts'; + +export function unique(name?: string): UniqueOnConstraintBuilder { + return new UniqueOnConstraintBuilder(name); +} + +export function uniqueKeyName(table: GoogleSqlTable, columns: string[]) { + return `${table[TableName]}_${columns.join('_')}_unique`; +} + +export class UniqueConstraintBuilder { + static readonly [entityKind]: string = 'GoogleSqlUniqueConstraintBuilder'; + + /** @internal */ + columns: GoogleSqlColumn[]; + + constructor( + columns: GoogleSqlColumn[], + private name?: string, + ) { + this.columns = columns; + } + + /** @internal */ + build(table: GoogleSqlTable): UniqueConstraint { + return new UniqueConstraint(table, this.columns, this.name); + } +} + +export class UniqueOnConstraintBuilder { + static readonly [entityKind]: string = 'GoogleSqlUniqueOnConstraintBuilder'; + + /** @internal */ + name?: string; + + constructor( + name?: string, + ) { + this.name = name; + } + + on(...columns: [GoogleSqlColumn, ...GoogleSqlColumn[]]) { + return new UniqueConstraintBuilder(columns, this.name); + } +} + +export class UniqueConstraint { + static readonly [entityKind]: string = 'GoogleSqlUniqueConstraint'; + + readonly columns: GoogleSqlColumn[]; + readonly name?: string; + readonly nullsNotDistinct: boolean = false; + + constructor(readonly table: GoogleSqlTable, columns: GoogleSqlColumn[], name?: string) { + this.columns = columns; + this.name = name ?? uniqueKeyName(this.table, this.columns.map((column) => column.name)); + } + + getName() { + return this.name; + } +} diff --git a/drizzle-orm/src/googlesql/utils.ts b/drizzle-orm/src/googlesql/utils.ts new file mode 100644 index 0000000000..90ed0c45a1 --- /dev/null +++ b/drizzle-orm/src/googlesql/utils.ts @@ -0,0 +1,80 @@ +import { is } from '~/entity.ts'; +import { Table } from '~/table.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import type { Check } from './checks.ts'; +import { CheckBuilder } from './checks.ts'; +import type { ForeignKey } from './foreign-keys.ts'; +import { ForeignKeyBuilder } from './foreign-keys.ts'; +import type { Index } from './indexes.ts'; +import { IndexBuilder } from './indexes.ts'; +import type { PrimaryKey } from './primary-keys.ts'; +import { PrimaryKeyBuilder } from './primary-keys.ts'; +import type { IndexForHint } from './query-builders/select.ts'; +import { GoogleSqlTable } from './table.ts'; +import { type UniqueConstraint, UniqueConstraintBuilder } from './unique-constraint.ts'; +import { GoogleSqlViewConfig } from './view-common.ts'; +import type { GoogleSqlView } from './view.ts'; + +export function getTableConfig(table: GoogleSqlTable) { + const columns = Object.values(table[GoogleSqlTable.Symbol.Columns]); + const indexes: Index[] = []; + const checks: Check[] = []; + const primaryKeys: PrimaryKey[] = []; + const uniqueConstraints: UniqueConstraint[] = []; + const foreignKeys: ForeignKey[] = Object.values(table[GoogleSqlTable.Symbol.InlineForeignKeys]); + const name = table[Table.Symbol.Name]; + const schema = table[Table.Symbol.Schema]; + const baseName = table[Table.Symbol.BaseName]; + + const extraConfigBuilder = table[GoogleSqlTable.Symbol.ExtraConfigBuilder]; + + if (extraConfigBuilder !== undefined) { + const extraConfig = extraConfigBuilder(table[GoogleSqlTable.Symbol.Columns]); + const extraValues = Array.isArray(extraConfig) ? extraConfig.flat(1) as any[] : Object.values(extraConfig); + for (const builder of Object.values(extraValues)) { + if (is(builder, IndexBuilder)) { + indexes.push(builder.build(table)); + } else if (is(builder, CheckBuilder)) { + checks.push(builder.build(table)); + } else if (is(builder, UniqueConstraintBuilder)) { + uniqueConstraints.push(builder.build(table)); + } else if (is(builder, PrimaryKeyBuilder)) { + primaryKeys.push(builder.build(table)); + } else if (is(builder, ForeignKeyBuilder)) { + foreignKeys.push(builder.build(table)); + } + } + } + + return { + columns, + indexes, + foreignKeys, + checks, + primaryKeys, + uniqueConstraints, + name, + schema, + baseName, + }; +} + +export function getViewConfig< + TName extends string = string, + TExisting extends boolean = boolean, +>(view: GoogleSqlView) { + return { + ...view[ViewBaseConfig], + ...view[GoogleSqlViewConfig], + }; +} + +export function convertIndexToString(indexes: IndexForHint[]) { + return indexes.map((idx) => { + return typeof idx === 'object' ? idx.config.name : idx; + }); +} + +export function toArray(value: T | T[]): T[] { + return Array.isArray(value) ? value : [value]; +} diff --git a/drizzle-orm/src/googlesql/view-base.ts b/drizzle-orm/src/googlesql/view-base.ts new file mode 100644 index 0000000000..0a836e24e9 --- /dev/null +++ b/drizzle-orm/src/googlesql/view-base.ts @@ -0,0 +1,15 @@ +import { entityKind } from '~/entity.ts'; +import type { ColumnsSelection } from '~/sql/sql.ts'; +import { View } from '~/sql/sql.ts'; + +export abstract class GoogleSqlViewBase< + TName extends string = string, + TExisting extends boolean = boolean, + TSelectedFields extends ColumnsSelection = ColumnsSelection, +> extends View { + static override readonly [entityKind]: string = 'GoogleSqlViewBase'; + + declare readonly _: View['_'] & { + readonly viewBrand: 'GoogleSqlViewBase'; + }; +} diff --git a/drizzle-orm/src/googlesql/view-common.ts b/drizzle-orm/src/googlesql/view-common.ts new file mode 100644 index 0000000000..c8060efdea --- /dev/null +++ b/drizzle-orm/src/googlesql/view-common.ts @@ -0,0 +1 @@ +export const GoogleSqlViewConfig = Symbol.for('drizzle:GoogleSqlViewConfig'); diff --git a/drizzle-orm/src/googlesql/view.ts b/drizzle-orm/src/googlesql/view.ts new file mode 100644 index 0000000000..4b69572f1c --- /dev/null +++ b/drizzle-orm/src/googlesql/view.ts @@ -0,0 +1,199 @@ +import type { BuildColumns } from '~/column-builder.ts'; +import { entityKind } from '~/entity.ts'; +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { AddAliasToSelection } from '~/query-builders/select.types.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; +import type { ColumnsSelection, SQL } from '~/sql/sql.ts'; +import { getTableColumns } from '~/utils.ts'; +import type { GoogleSqlColumn, GoogleSqlColumnBuilderBase } from './columns/index.ts'; +import { QueryBuilder } from './query-builders/query-builder.ts'; +import { googlesqlTable } from './table.ts'; +import { GoogleSqlViewBase } from './view-base.ts'; +import { GoogleSqlViewConfig } from './view-common.ts'; + +export interface ViewBuilderConfig { + algorithm?: 'undefined' | 'merge' | 'temptable'; + sqlSecurity?: 'definer' | 'invoker'; + withCheckOption?: 'cascaded' | 'local'; +} + +export class ViewBuilderCore { + static readonly [entityKind]: string = 'GoogleSqlViewBuilder'; + + declare readonly _: { + readonly name: TConfig['name']; + readonly columns: TConfig['columns']; + }; + + constructor( + protected name: TConfig['name'], + protected schema: string | undefined, + ) {} + + protected config: ViewBuilderConfig = {}; + + algorithm( + algorithm: Exclude, + ): this { + this.config.algorithm = algorithm; + return this; + } + + sqlSecurity( + sqlSecurity: Exclude, + ): this { + this.config.sqlSecurity = sqlSecurity; + return this; + } + + withCheckOption( + withCheckOption?: Exclude, + ): this { + this.config.withCheckOption = withCheckOption ?? 'cascaded'; + return this; + } +} + +export class ViewBuilder extends ViewBuilderCore<{ name: TName }> { + static override readonly [entityKind]: string = 'GoogleSqlViewBuilder'; + + as( + qb: TypedQueryBuilder | ((qb: QueryBuilder) => TypedQueryBuilder), + ): GoogleSqlViewWithSelection> { + if (typeof qb === 'function') { + qb = qb(new QueryBuilder()); + } + const selectionProxy = new SelectionProxyHandler({ + alias: this.name, + sqlBehavior: 'error', + sqlAliasedBehavior: 'alias', + replaceOriginalName: true, + }); + const aliasedSelection = new Proxy(qb.getSelectedFields(), selectionProxy); + return new Proxy( + new GoogleSqlView({ + googlesqlConfig: this.config, + config: { + name: this.name, + schema: this.schema, + selectedFields: aliasedSelection, + query: qb.getSQL().inlineParams(), + }, + }), + selectionProxy as any, + ) as GoogleSqlViewWithSelection>; + } +} + +export class ManualViewBuilder< + TName extends string = string, + TColumns extends Record = Record, +> extends ViewBuilderCore<{ name: TName; columns: TColumns }> { + static override readonly [entityKind]: string = 'GoogleSqlManualViewBuilder'; + + private columns: Record; + + constructor( + name: TName, + columns: TColumns, + schema: string | undefined, + ) { + super(name, schema); + this.columns = getTableColumns(googlesqlTable(name, columns)) as BuildColumns; + } + + existing(): GoogleSqlViewWithSelection> { + return new Proxy( + new GoogleSqlView({ + googlesqlConfig: undefined, + config: { + name: this.name, + schema: this.schema, + selectedFields: this.columns, + query: undefined, + }, + }), + new SelectionProxyHandler({ + alias: this.name, + sqlBehavior: 'error', + sqlAliasedBehavior: 'alias', + replaceOriginalName: true, + }), + ) as GoogleSqlViewWithSelection>; + } + + as(query: SQL): GoogleSqlViewWithSelection> { + return new Proxy( + new GoogleSqlView({ + googlesqlConfig: this.config, + config: { + name: this.name, + schema: this.schema, + selectedFields: this.columns, + query: query.inlineParams(), + }, + }), + new SelectionProxyHandler({ + alias: this.name, + sqlBehavior: 'error', + sqlAliasedBehavior: 'alias', + replaceOriginalName: true, + }), + ) as GoogleSqlViewWithSelection>; + } +} + +export class GoogleSqlView< + TName extends string = string, + TExisting extends boolean = boolean, + TSelectedFields extends ColumnsSelection = ColumnsSelection, +> extends GoogleSqlViewBase { + static override readonly [entityKind]: string = 'GoogleSqlView'; + + declare protected $GoogleSqlViewBrand: 'GoogleSqlView'; + + [GoogleSqlViewConfig]: ViewBuilderConfig | undefined; + + constructor({ googlesqlConfig, config }: { + googlesqlConfig: ViewBuilderConfig | undefined; + config: { + name: TName; + schema: string | undefined; + selectedFields: ColumnsSelection; + query: SQL | undefined; + }; + }) { + super(config); + this[GoogleSqlViewConfig] = googlesqlConfig; + } +} + +export type GoogleSqlViewWithSelection< + TName extends string, + TExisting extends boolean, + TSelectedFields extends ColumnsSelection, +> = GoogleSqlView & TSelectedFields; + +/** @internal */ +export function googlesqlViewWithSchema( + name: string, + selection: Record | undefined, + schema: string | undefined, +): ViewBuilder | ManualViewBuilder { + if (selection) { + return new ManualViewBuilder(name, selection, schema); + } + return new ViewBuilder(name, schema); +} + +export function googlesqlView(name: TName): ViewBuilder; +export function googlesqlView>( + name: TName, + columns: TColumns, +): ManualViewBuilder; +export function googlesqlView( + name: string, + selection?: Record, +): ViewBuilder | ManualViewBuilder { + return googlesqlViewWithSchema(name, selection, undefined); +} diff --git a/drizzle-orm/src/spanner/driver.ts b/drizzle-orm/src/spanner/driver.ts new file mode 100644 index 0000000000..6602d22046 --- /dev/null +++ b/drizzle-orm/src/spanner/driver.ts @@ -0,0 +1,172 @@ +import { type Connection as CallbackConnection, createPool, type Pool as CallbackPool, type PoolOptions } from 'mysql2'; +import type { Connection, Pool } from 'mysql2/promise'; +import { entityKind } from '~/entity.ts'; +import type { Logger } from '~/logger.ts'; +import { DefaultLogger } from '~/logger.ts'; +import { GoogleSqlDatabase } from '~/googlesql/db.ts'; +import { GoogleSqlDialect } from '~/googlesql/dialect.ts'; +import type { Mode } from '~/googlesql/session.ts'; +import { + createTableRelationsHelpers, + extractTablesRelationalConfig, + type RelationalSchemaConfig, + type TablesRelationalConfig, +} from '~/relations.ts'; +import { type DrizzleConfig, isConfig } from '~/utils.ts'; +import { DrizzleError } from '../errors.ts'; +import type { SpannerClient, SpannerPreparedQueryHKT, SpannerQueryResultHKT } from './session.ts'; +import { SpannerSession } from './session.ts'; + +export interface GoogleSqlDriverOptions { + logger?: Logger; +} + +export class SpannerDriver { + static readonly [entityKind]: string = 'SpannerDriver'; + + constructor( + private client: SpannerClient, + private dialect: GoogleSqlDialect, + private options: GoogleSqlDriverOptions = {}, + ) { + } + + createSession( + schema: RelationalSchemaConfig | undefined, + mode: Mode, + ): SpannerSession, TablesRelationalConfig> { + return new SpannerSession(this.client, this.dialect, schema, { logger: this.options.logger, mode }); + } +} + +export { GoogleSqlDatabase } from '~/googlesql/db.ts'; + +export class SpannerDatabase< + TSchema extends Record = Record, +> extends GoogleSqlDatabase { + static override readonly [entityKind]: string = 'SpannerDatabase'; +} + +export type SpannerDrizzleConfig = Record> = + & Omit, 'schema'> + & ({ schema: TSchema; mode: Mode } | { schema?: undefined; mode?: Mode }); + +function construct< + TSchema extends Record = Record, + TClient extends Pool | Connection | CallbackPool | CallbackConnection = CallbackPool, +>( + client: TClient, + config: SpannerDrizzleConfig = {}, +): SpannerDatabase & { + $client: TClient; +} { + const dialect = new GoogleSqlDialect({ casing: config.casing }); + let logger; + if (config.logger === true) { + logger = new DefaultLogger(); + } else if (config.logger !== false) { + logger = config.logger; + } + + const clientForInstance = isCallbackClient(client) ? client.promise() : client; + + let schema: RelationalSchemaConfig | undefined; + if (config.schema) { + if (config.mode === undefined) { + throw new DrizzleError({ + message: + 'You need to specify "mode": "planetscale" or "default" when providing a schema. Read more: https://orm.drizzle.team/docs/rqb#modes', + }); + } + + const tablesConfig = extractTablesRelationalConfig( + config.schema, + createTableRelationsHelpers, + ); + schema = { + fullSchema: config.schema, + schema: tablesConfig.tables, + tableNamesMap: tablesConfig.tableNamesMap, + }; + } + + const mode = config.mode ?? 'default'; + + const driver = new SpannerDriver(clientForInstance as SpannerClient, dialect, { logger }); + const session = driver.createSession(schema, mode); + const db = new SpannerDatabase(dialect, session, schema as any, mode) as SpannerDatabase; + ( db).$client = client; + + return db as any; +} + +interface CallbackClient { + promise(): SpannerClient; +} + +function isCallbackClient(client: any): client is CallbackClient { + return typeof client.promise === 'function'; +} + +export type AnySpannerConnection = Pool | Connection | CallbackPool | CallbackConnection; + +export function drizzle< + TSchema extends Record = Record, + TClient extends AnySpannerConnection = CallbackPool, +>( + ...params: [ + TClient | string, + ] | [ + TClient | string, + SpannerDrizzleConfig, + ] | [ + ( + & SpannerDrizzleConfig + & ({ + connection: string | PoolOptions; + } | { + client: TClient; + }) + ), + ] +): SpannerDatabase & { + $client: TClient; +} { + if (typeof params[0] === 'string') { + const connectionString = params[0]!; + const instance = createPool({ + uri: connectionString, + }); + + return construct(instance, params[1]) as any; + } + + if (isConfig(params[0])) { + const { connection, client, ...drizzleConfig } = params[0] as + & { connection?: PoolOptions | string; client?: TClient } + & SpannerDrizzleConfig; + + if (client) return construct(client, drizzleConfig) as any; + + const instance = typeof connection === 'string' + ? createPool({ + uri: connection, + }) + : createPool(connection!); + const db = construct(instance, drizzleConfig); + + return db as any; + } + + return construct(params[0] as TClient, params[1] as SpannerDrizzleConfig | undefined) as any; +} + +export namespace drizzle { + export function mock = Record>( + config?: SpannerDrizzleConfig, + ): SpannerDatabase & { + $client: '$client is not available on drizzle.mock()'; + } { + return construct({} as any, config) as any; + } +} diff --git a/drizzle-orm/src/spanner/index.ts b/drizzle-orm/src/spanner/index.ts new file mode 100644 index 0000000000..b1b6a52e71 --- /dev/null +++ b/drizzle-orm/src/spanner/index.ts @@ -0,0 +1,2 @@ +export * from './driver.ts'; +export * from './session.ts'; diff --git a/drizzle-orm/src/spanner/migrator.ts b/drizzle-orm/src/spanner/migrator.ts new file mode 100644 index 0000000000..65d6e9fef6 --- /dev/null +++ b/drizzle-orm/src/spanner/migrator.ts @@ -0,0 +1,11 @@ +import type { MigrationConfig } from '~/migrator.ts'; +import { readMigrationFiles } from '~/migrator.ts'; +import type { SpannerDatabase } from './driver.ts'; + +export async function migrate>( + db: SpannerDatabase, + config: MigrationConfig, +) { + const migrations = readMigrationFiles(config); + await db.dialect.migrate(migrations, db.session, config); +} diff --git a/drizzle-orm/src/spanner/session.ts b/drizzle-orm/src/spanner/session.ts new file mode 100644 index 0000000000..93c0e84dad --- /dev/null +++ b/drizzle-orm/src/spanner/session.ts @@ -0,0 +1,337 @@ +import type { Connection as CallbackConnection } from 'mysql2'; +import type { + Connection, + FieldPacket, + OkPacket, + Pool, + PoolConnection, + QueryOptions, + ResultSetHeader, + RowDataPacket, +} from 'mysql2/promise'; +import { once } from 'node:events'; +import { Column } from '~/column.ts'; +import { entityKind, is } from '~/entity.ts'; +import type { Logger } from '~/logger.ts'; +import { NoopLogger } from '~/logger.ts'; +import type { GoogleSqlDialect } from '~/googlesql/dialect.ts'; +import type { SelectedFieldsOrdered } from '~/googlesql/query-builders/select.types.ts'; +import { + type Mode, + GoogleSqlPreparedQuery, + type GoogleSqlPreparedQueryConfig, + type GoogleSqlPreparedQueryHKT, + type GoogleSqlQueryResultHKT, + GoogleSqlSession, + GoogleSqlTransaction, + type GoogleSqlTransactionConfig, + type PreparedQueryKind, +} from '~/googlesql/session.ts'; +import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; +import { fillPlaceholders, sql } from '~/sql/sql.ts'; +import type { Query, SQL } from '~/sql/sql.ts'; +import { type Assume, mapResultRow } from '~/utils.ts'; + +export type SpannerClient = Pool | Connection; + +export type GoogleSqlRawQueryResult = [ResultSetHeader, FieldPacket[]]; +export type GoogleSqlQueryResultType = RowDataPacket[][] | RowDataPacket[] | OkPacket | OkPacket[] | ResultSetHeader; +export type GoogleSqlQueryResult< + T = any, +> = [T extends ResultSetHeader ? T : T[], FieldPacket[]]; + +export class SpannerPreparedQuery extends GoogleSqlPreparedQuery { + static override readonly [entityKind]: string = 'SpannerPreparedQuery'; + + private rawQuery: QueryOptions; + private query: QueryOptions; + + constructor( + private client: SpannerClient, + queryString: string, + private params: unknown[], + private logger: Logger, + private fields: SelectedFieldsOrdered | undefined, + private customResultMapper?: (rows: unknown[][]) => T['execute'], + // Keys that were used in $default and the value that was generated for them + private generatedIds?: Record[], + // Keys that should be returned, it has the column with all properries + key from object + private returningIds?: SelectedFieldsOrdered, + ) { + super(); + this.rawQuery = { + sql: queryString, + // rowsAsArray: true, + typeCast: function(field: any, next: any) { + if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') { + return field.string(); + } + return next(); + }, + }; + this.query = { + sql: queryString, + rowsAsArray: true, + typeCast: function(field: any, next: any) { + if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') { + return field.string(); + } + return next(); + }, + }; + } + + async execute(placeholderValues: Record = {}): Promise { + const params = fillPlaceholders(this.params, placeholderValues); + + this.logger.logQuery(this.rawQuery.sql, params); + + const { fields, client, rawQuery, query, joinsNotNullableMap, customResultMapper, returningIds, generatedIds } = + this; + if (!fields && !customResultMapper) { + const res = await client.query(rawQuery, params); + const insertId = res[0].insertId; + const affectedRows = res[0].affectedRows; + // for each row, I need to check keys from + if (returningIds) { + const returningResponse = []; + let j = 0; + for (let i = insertId; i < insertId + affectedRows; i++) { + for (const column of returningIds) { + const key = returningIds[0]!.path[0]!; + if (is(column.field, Column)) { + // @ts-ignore + if (column.field.primary && column.field.autoIncrement) { + returningResponse.push({ [key]: i }); + } + if (column.field.defaultFn && generatedIds) { + // generatedIds[rowIdx][key] + returningResponse.push({ [key]: generatedIds[j]![key] }); + } + } + } + j++; + } + + return returningResponse; + } + return res; + } + + const result = await client.query(query, params); + const rows = result[0]; + + if (customResultMapper) { + return customResultMapper(rows); + } + + return rows.map((row) => mapResultRow(fields!, row, joinsNotNullableMap)); + } + + async *iterator( + placeholderValues: Record = {}, + ): AsyncGenerator { + const params = fillPlaceholders(this.params, placeholderValues); + const conn = ((isPool(this.client) ? await this.client.getConnection() : this.client) as {} as { + connection: CallbackConnection; + }).connection; + + const { fields, query, rawQuery, joinsNotNullableMap, client, customResultMapper } = this; + const hasRowsMapper = Boolean(fields || customResultMapper); + const driverQuery = hasRowsMapper ? conn.query(query, params) : conn.query(rawQuery, params); + + const stream = driverQuery.stream(); + + function dataListener() { + stream.pause(); + } + + stream.on('data', dataListener); + + try { + const onEnd = once(stream, 'end'); + const onError = once(stream, 'error'); + + while (true) { + stream.resume(); + const row = await Promise.race([onEnd, onError, new Promise((resolve) => stream.once('data', resolve))]); + if (row === undefined || (Array.isArray(row) && row.length === 0)) { + break; + } else if (row instanceof Error) { // eslint-disable-line no-instanceof/no-instanceof + throw row; + } else { + if (hasRowsMapper) { + if (customResultMapper) { + const mappedRow = customResultMapper([row as unknown[]]); + yield (Array.isArray(mappedRow) ? mappedRow[0] : mappedRow); + } else { + yield mapResultRow(fields!, row as unknown[], joinsNotNullableMap); + } + } else { + yield row as T['execute']; + } + } + } + } finally { + stream.off('data', dataListener); + if (isPool(client)) { + conn.end(); + } + } + } +} + +export interface SpannerSessionOptions { + logger?: Logger; + mode: Mode; +} + +export class SpannerSession< + TFullSchema extends Record, + TSchema extends TablesRelationalConfig, +> extends GoogleSqlSession { + static override readonly [entityKind]: string = 'SpannerSession'; + + private logger: Logger; + private mode: Mode; + + constructor( + private client: SpannerClient, + dialect: GoogleSqlDialect, + private schema: RelationalSchemaConfig | undefined, + private options: SpannerSessionOptions, + ) { + super(dialect); + this.logger = options.logger ?? new NoopLogger(); + this.mode = options.mode; + } + + prepareQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + customResultMapper?: (rows: unknown[][]) => T['execute'], + generatedIds?: Record[], + returningIds?: SelectedFieldsOrdered, + ): PreparedQueryKind { + // Add returningId fields + // Each driver gets them from response from database + return new SpannerPreparedQuery( + this.client, + query.sql, + query.params, + this.logger, + fields, + customResultMapper, + generatedIds, + returningIds, + ) as PreparedQueryKind; + } + + /** + * @internal + * What is its purpose? + */ + async query(query: string, params: unknown[]): Promise { + this.logger.logQuery(query, params); + const result = await this.client.query({ + sql: query, + values: params, + rowsAsArray: true, + typeCast: function(field: any, next: any) { + if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') { + return field.string(); + } + return next(); + }, + }); + return result; + } + + override all(query: SQL): Promise { + const querySql = this.dialect.sqlToQuery(query); + this.logger.logQuery(querySql.sql, querySql.params); + return this.client.execute(querySql.sql, querySql.params).then((result) => result[0]) as Promise; + } + + override async transaction( + transaction: (tx: SpannerTransaction) => Promise, + config?: GoogleSqlTransactionConfig, + ): Promise { + const session = isPool(this.client) + ? new SpannerSession( + await this.client.getConnection(), + this.dialect, + this.schema, + this.options, + ) + : this; + const tx = new SpannerTransaction( + this.dialect, + session as GoogleSqlSession, + this.schema, + 0, + this.mode, + ); + if (config) { + const setTransactionConfigSql = this.getSetTransactionSQL(config); + if (setTransactionConfigSql) { + await tx.execute(setTransactionConfigSql); + } + const startTransactionSql = this.getStartTransactionSQL(config); + await (startTransactionSql ? tx.execute(startTransactionSql) : tx.execute(sql`begin`)); + } else { + await tx.execute(sql`begin`); + } + try { + const result = await transaction(tx); + await tx.execute(sql`commit`); + return result; + } catch (err) { + await tx.execute(sql`rollback`); + throw err; + } finally { + if (isPool(this.client)) { + (session.client as PoolConnection).release(); + } + } + } +} + +export class SpannerTransaction< + TFullSchema extends Record, + TSchema extends TablesRelationalConfig, +> extends GoogleSqlTransaction { + static override readonly [entityKind]: string = 'SpannerTransaction'; + + override async transaction(transaction: (tx: SpannerTransaction) => Promise): Promise { + const savepointName = `sp${this.nestedIndex + 1}`; + const tx = new SpannerTransaction( + this.dialect, + this.session, + this.schema, + this.nestedIndex + 1, + this.mode, + ); + await tx.execute(sql.raw(`savepoint ${savepointName}`)); + try { + const result = await transaction(tx); + await tx.execute(sql.raw(`release savepoint ${savepointName}`)); + return result; + } catch (err) { + await tx.execute(sql.raw(`rollback to savepoint ${savepointName}`)); + throw err; + } + } +} + +function isPool(client: SpannerClient): client is Pool { + return 'getConnection' in client; +} + +export interface SpannerQueryResultHKT extends GoogleSqlQueryResultHKT { + type: GoogleSqlRawQueryResult; +} + +export interface SpannerPreparedQueryHKT extends GoogleSqlPreparedQueryHKT { + type: SpannerPreparedQuery>; +} diff --git a/drizzle-orm/tests/casing/googlesql-to-camel.test.ts b/drizzle-orm/tests/casing/googlesql-to-camel.test.ts new file mode 100644 index 0000000000..a417c40967 --- /dev/null +++ b/drizzle-orm/tests/casing/googlesql-to-camel.test.ts @@ -0,0 +1,245 @@ +import { beforeEach, describe, it } from 'vitest'; +import { alias, boolean, int, googlesqlSchema, googlesqlTable, serial, text, union } from '~/googlesql'; +import { drizzle as spanner } from '~/spanner'; +import { relations } from '~/relations'; +import { asc, eq, sql } from '~/sql'; +import { createPool } from 'mysql2'; + +const testSchema = googlesqlSchema('test'); +const users = googlesqlTable('users', { + id: serial().primaryKey(), + first_name: text().notNull(), + last_name: text().notNull(), + // Test that custom aliases remain + age: int('AGE'), +}); +const usersRelations = relations(users, ({ one }) => ({ + developers: one(developers), +})); +const developers = testSchema.table('developers', { + user_id: serial().primaryKey().references(() => users.id), + uses_drizzle_orm: boolean().notNull(), +}); +const developersRelations = relations(developers, ({ one }) => ({ + user: one(users, { + fields: [developers.user_id], + references: [users.id], + }), +})); +const devs = alias(developers, 'devs'); +const schema = { users, usersRelations, developers, developersRelations }; + + +const instance = createPool({ + uri: "mysql://root:password@localhost:3306/test", +}); + +const db = spanner({ client: instance, schema: schema, casing: 'camelCase', mode: "default" }); + +const usersCache = { + 'public.users.id': 'id', + 'public.users.first_name': 'firstName', + 'public.users.last_name': 'lastName', + 'public.users.AGE': 'age', +}; +const developersCache = { + 'test.developers.user_id': 'userId', + 'test.developers.uses_drizzle_orm': 'usesDrizzleOrm', +}; +const cache = { + ...usersCache, + ...developersCache, +}; + +const fullName = sql`${users.first_name} || ' ' || ${users.last_name}`.as('name'); + +describe('mysql to snake case', () => { + beforeEach(() => { + db.dialect.casing.clearCache(); + }); + + it('select', ({ expect }) => { + const query = db + .select({ name: fullName, age: users.age }) + .from(users) + .leftJoin(developers, eq(users.id, developers.user_id)) + .orderBy(asc(users.first_name)); + + expect(query.toSQL()).toEqual({ + sql: + "select `users`.`firstName` || ' ' || `users`.`lastName` as `name`, `users`.`AGE` from `users` left join `test`.`developers` on `users`.`id` = `test`.`developers`.`userId` order by `users`.`firstName` asc", + params: [], + }); + expect(db.dialect.casing.cache).toEqual(cache); + }); + + it('select (with alias)', ({ expect }) => { + const query = db + .select({ firstName: users.first_name }) + .from(users) + .leftJoin(devs, eq(users.id, devs.user_id)); + + expect(query.toSQL()).toEqual({ + sql: + 'select `users`.`firstName` from `users` left join `test`.`developers` `devs` on `users`.`id` = `devs`.`userId`', + params: [], + }); + expect(db.dialect.casing.cache).toEqual(cache); + }); + + it('with CTE', ({ expect }) => { + const cte = db.$with('cte').as(db.select({ name: fullName }).from(users)); + const query = db.with(cte).select().from(cte); + + expect(query.toSQL()).toEqual({ + sql: "with `cte` as (select `firstName` || ' ' || `lastName` as `name` from `users`) select `name` from `cte`", + params: [], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('with CTE (with query builder)', ({ expect }) => { + const cte = db.$with('cte').as((qb) => qb.select({ name: fullName }).from(users)); + const query = db.with(cte).select().from(cte); + + expect(query.toSQL()).toEqual({ + sql: "with `cte` as (select `firstName` || ' ' || `lastName` as `name` from `users`) select `name` from `cte`", + params: [], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('set operator', ({ expect }) => { + const query = db + .select({ firstName: users.first_name }) + .from(users) + .union(db.select({ firstName: users.first_name }).from(users)); + + expect(query.toSQL()).toEqual({ + sql: '(select `firstName` from `users`) union (select `firstName` from `users`)', + params: [], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('set operator (function)', ({ expect }) => { + const query = union( + db.select({ firstName: users.first_name }).from(users), + db.select({ firstName: users.first_name }).from(users), + ); + + expect(query.toSQL()).toEqual({ + sql: '(select `firstName` from `users`) union (select `firstName` from `users`)', + params: [], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('query (find first)', ({ expect }) => { + const query = db.query.users.findFirst({ + columns: { + id: true, + age: true, + }, + extras: { + fullName, + }, + where: eq(users.id, 1), + with: { + developers: { + columns: { + uses_drizzle_orm: true, + }, + }, + }, + }); + + expect(query.toSQL()).toEqual({ + sql: + "select `users`.`id`, `users`.`AGE`, `users`.`firstName` || ' ' || `users`.`lastName` as `name`, `users_developers`.`data` as `developers` from `users` left join lateral (select json_array(`users_developers`.`usesDrizzleOrm`) as `data` from (select * from `developers` `users_developers` where `users_developers`.`userId` = `users`.`id` limit ?) `users_developers`) `users_developers` on true where `users`.`id` = ? limit ?", + params: [1, 1, 1], + typings: ['none', 'none', 'none'], + }); + expect(db.dialect.casing.cache).toEqual(cache); + }); + + it('query (find many)', ({ expect }) => { + const query = db.query.users.findMany({ + columns: { + id: true, + age: true, + }, + extras: { + fullName, + }, + where: eq(users.id, 1), + with: { + developers: { + columns: { + uses_drizzle_orm: true, + }, + }, + }, + }); + + expect(query.toSQL()).toEqual({ + sql: + "select `users`.`id`, `users`.`AGE`, `users`.`firstName` || ' ' || `users`.`lastName` as `name`, `users_developers`.`data` as `developers` from `users` left join lateral (select json_array(`users_developers`.`usesDrizzleOrm`) as `data` from (select * from `developers` `users_developers` where `users_developers`.`userId` = `users`.`id` limit ?) `users_developers`) `users_developers` on true where `users`.`id` = ?", + params: [1, 1], + typings: ['none', 'none'], + }); + expect(db.dialect.casing.cache).toEqual(cache); + }); + + + it('insert', ({ expect }) => { + const query = db + .insert(users) + .values({ first_name: 'John', last_name: 'Doe', age: 30 }); + + expect(query.toSQL()).toEqual({ + sql: 'insert into `users` (`id`, `firstName`, `lastName`, `AGE`) values (default, ?, ?, ?)', + params: ['John', 'Doe', 30], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('insert (on duplicate key update)', ({ expect }) => { + const query = db + .insert(users) + .values({ first_name: 'John', last_name: 'Doe', age: 30 }) + .onDuplicateKeyUpdate({ set: { age: 31 } }); + + expect(query.toSQL()).toEqual({ + sql: + 'insert into `users` (`id`, `firstName`, `lastName`, `AGE`) values (default, ?, ?, ?) on duplicate key update `AGE` = ?', + params: ['John', 'Doe', 30, 31], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('update', ({ expect }) => { + const query = db + .update(users) + .set({ first_name: 'John', last_name: 'Doe', age: 30 }) + .where(eq(users.id, 1)); + + expect(query.toSQL()).toEqual({ + sql: 'update `users` set `firstName` = ?, `lastName` = ?, `AGE` = ? where `users`.`id` = ?', + params: ['John', 'Doe', 30, 1], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('delete', ({ expect }) => { + const query = db + .delete(users) + .where(eq(users.id, 1)); + + expect(query.toSQL()).toEqual({ + sql: 'delete from `users` where `users`.`id` = ?', + params: [1], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); +}); diff --git a/drizzle-orm/tests/casing/googlesql-to-snake.test.ts b/drizzle-orm/tests/casing/googlesql-to-snake.test.ts new file mode 100644 index 0000000000..826045f8c4 --- /dev/null +++ b/drizzle-orm/tests/casing/googlesql-to-snake.test.ts @@ -0,0 +1,247 @@ +import { createPool } from 'mysql2'; +import { beforeEach, describe, it } from 'vitest'; +import { alias, boolean, int, googlesqlSchema, googlesqlTable, serial, text, union } from '~/googlesql'; +import { drizzle as spanner } from '~/spanner'; +import { relations } from '~/relations'; +import { asc, eq, sql } from '~/sql'; + +const testSchema = googlesqlSchema('test'); +const users = googlesqlTable('users', { + id: serial().primaryKey(), + firstName: text().notNull(), + lastName: text().notNull(), + // Test that custom aliases remain + age: int('AGE'), +}); +const usersRelations = relations(users, ({ one }) => ({ + developers: one(developers), +})); +const developers = testSchema.table('developers', { + userId: serial().primaryKey().references(() => users.id), + usesDrizzleORM: boolean().notNull(), +}); +const developersRelations = relations(developers, ({ one }) => ({ + user: one(users, { + fields: [developers.userId], + references: [users.id], + }), +})); +const devs = alias(developers, 'devs'); +const schema = { users, usersRelations, developers, developersRelations }; + + + +const instance = createPool({ + uri: "googlesql://root:password@localhost:3306/test", +}); + +const db = spanner({ client: instance, schema: schema, casing: 'snake_case', mode: "default" }); + +const usersCache = { + 'public.users.id': 'id', + 'public.users.firstName': 'first_name', + 'public.users.lastName': 'last_name', + 'public.users.AGE': 'age', +}; +const developersCache = { + 'test.developers.userId': 'user_id', + 'test.developers.usesDrizzleORM': 'uses_drizzle_orm', +}; +const cache = { + ...usersCache, + ...developersCache, +}; + +const fullName = sql`${users.firstName} || ' ' || ${users.lastName}`.as('name'); + +describe('googlesql to snake case', () => { + beforeEach(() => { + db.dialect.casing.clearCache(); + }); + + it('select', ({ expect }) => { + const query = db + .select({ name: fullName, age: users.age }) + .from(users) + .leftJoin(developers, eq(users.id, developers.userId)) + .orderBy(asc(users.firstName)); + + expect(query.toSQL()).toEqual({ + sql: + "select `users`.`first_name` || ' ' || `users`.`last_name` as `name`, `users`.`AGE` from `users` left join `test`.`developers` on `users`.`id` = `test`.`developers`.`user_id` order by `users`.`first_name` asc", + params: [], + }); + expect(db.dialect.casing.cache).toEqual(cache); + }); + + it('select (with alias)', ({ expect }) => { + const query = db + .select({ firstName: users.firstName }) + .from(users) + .leftJoin(devs, eq(users.id, devs.userId)); + + expect(query.toSQL()).toEqual({ + sql: + 'select `users`.`first_name` from `users` left join `test`.`developers` `devs` on `users`.`id` = `devs`.`user_id`', + params: [], + }); + expect(db.dialect.casing.cache).toEqual(cache); + }); + + it('with CTE', ({ expect }) => { + const cte = db.$with('cte').as(db.select({ name: fullName }).from(users)); + const query = db.with(cte).select().from(cte); + + expect(query.toSQL()).toEqual({ + sql: "with `cte` as (select `first_name` || ' ' || `last_name` as `name` from `users`) select `name` from `cte`", + params: [], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('with CTE (with query builder)', ({ expect }) => { + const cte = db.$with('cte').as((qb) => qb.select({ name: fullName }).from(users)); + const query = db.with(cte).select().from(cte); + + expect(query.toSQL()).toEqual({ + sql: "with `cte` as (select `first_name` || ' ' || `last_name` as `name` from `users`) select `name` from `cte`", + params: [], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('set operator', ({ expect }) => { + const query = db + .select({ firstName: users.firstName }) + .from(users) + .union(db.select({ firstName: users.firstName }).from(users)); + + expect(query.toSQL()).toEqual({ + sql: '(select `first_name` from `users`) union (select `first_name` from `users`)', + params: [], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('set operator (function)', ({ expect }) => { + const query = union( + db.select({ firstName: users.firstName }).from(users), + db.select({ firstName: users.firstName }).from(users), + ); + + expect(query.toSQL()).toEqual({ + sql: '(select `first_name` from `users`) union (select `first_name` from `users`)', + params: [], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('query (find first)', ({ expect }) => { + const query = db.query.users.findFirst({ + columns: { + id: true, + age: true, + }, + extras: { + fullName, + }, + where: eq(users.id, 1), + with: { + developers: { + columns: { + usesDrizzleORM: true, + }, + }, + }, + }); + + expect(query.toSQL()).toEqual({ + sql: + "select `users`.`id`, `users`.`AGE`, `users`.`first_name` || ' ' || `users`.`last_name` as `name`, `users_developers`.`data` as `developers` from `users` left join lateral (select json_array(`users_developers`.`uses_drizzle_orm`) as `data` from (select * from `developers` `users_developers` where `users_developers`.`user_id` = `users`.`id` limit ?) `users_developers`) `users_developers` on true where `users`.`id` = ? limit ?", + params: [1, 1, 1], + typings: ['none', 'none', 'none'], + }); + expect(db.dialect.casing.cache).toEqual(cache); + }); + + + it('query (find many)', ({ expect }) => { + const query = db.query.users.findMany({ + columns: { + id: true, + age: true, + }, + extras: { + fullName, + }, + where: eq(users.id, 1), + with: { + developers: { + columns: { + usesDrizzleORM: true, + }, + }, + }, + }); + + expect(query.toSQL()).toEqual({ + sql: + "select `users`.`id`, `users`.`AGE`, `users`.`first_name` || ' ' || `users`.`last_name` as `name`, `users_developers`.`data` as `developers` from `users` left join lateral (select json_array(`users_developers`.`uses_drizzle_orm`) as `data` from (select * from `developers` `users_developers` where `users_developers`.`user_id` = `users`.`id` limit ?) `users_developers`) `users_developers` on true where `users`.`id` = ?", + params: [1, 1], + typings: ['none', 'none'], + }); + expect(db.dialect.casing.cache).toEqual(cache); + }); + + + it('insert', ({ expect }) => { + const query = db + .insert(users) + .values({ firstName: 'John', lastName: 'Doe', age: 30 }); + + expect(query.toSQL()).toEqual({ + sql: 'insert into `users` (`id`, `first_name`, `last_name`, `AGE`) values (default, ?, ?, ?)', + params: ['John', 'Doe', 30], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('insert (on duplicate key update)', ({ expect }) => { + const query = db + .insert(users) + .values({ firstName: 'John', lastName: 'Doe', age: 30 }) + .onDuplicateKeyUpdate({ set: { age: 31 } }); + + expect(query.toSQL()).toEqual({ + sql: + 'insert into `users` (`id`, `first_name`, `last_name`, `AGE`) values (default, ?, ?, ?) on duplicate key update `AGE` = ?', + params: ['John', 'Doe', 30, 31], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('update', ({ expect }) => { + const query = db + .update(users) + .set({ firstName: 'John', lastName: 'Doe', age: 30 }) + .where(eq(users.id, 1)); + + expect(query.toSQL()).toEqual({ + sql: 'update `users` set `first_name` = ?, `last_name` = ?, `AGE` = ? where `users`.`id` = ?', + params: ['John', 'Doe', 30, 1], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); + + it('delete', ({ expect }) => { + const query = db + .delete(users) + .where(eq(users.id, 1)); + + expect(query.toSQL()).toEqual({ + sql: 'delete from `users` where `users`.`id` = ?', + params: [1], + }); + expect(db.dialect.casing.cache).toEqual(usersCache); + }); +});