From fc8825de7ddef0a5c2e0caffc946c8645776d62a Mon Sep 17 00:00:00 2001 From: BridgerB Date: Sun, 7 Dec 2025 13:10:58 -0700 Subject: [PATCH 1/2] Add BigQuery dialect and driver support - Add bigquery-core/ dialect with columns, query builders, and SQL generation - Add bigquery/ driver wrapping @google-cloud/bigquery client - Support SELECT, INSERT, UPDATE, DELETE operations - Support JOINs, aggregates, ordering, limits, offsets - Add \$count() helper, alias(), subquery support - Add migration support via migrate() function - Add integration tests (28 passing tests) Closes #2001 --- drizzle-orm/package.json | 7 +- drizzle-orm/src/bigquery-core/alias.ts | 12 + drizzle-orm/src/bigquery-core/checks.ts | 32 + drizzle-orm/src/bigquery-core/columns/all.ts | 30 + drizzle-orm/src/bigquery-core/columns/bool.ts | 48 + .../src/bigquery-core/columns/bytes.ts | 81 ++ .../src/bigquery-core/columns/common.ts | 85 ++ drizzle-orm/src/bigquery-core/columns/date.ts | 115 +++ .../src/bigquery-core/columns/datetime.ts | 119 +++ .../src/bigquery-core/columns/float64.ts | 48 + .../src/bigquery-core/columns/index.ts | 13 + .../src/bigquery-core/columns/int64.ts | 118 +++ drizzle-orm/src/bigquery-core/columns/json.ts | 59 ++ .../src/bigquery-core/columns/numeric.ts | 152 ++++ .../src/bigquery-core/columns/string.ts | 65 ++ drizzle-orm/src/bigquery-core/columns/time.ts | 53 ++ .../src/bigquery-core/columns/timestamp.ts | 118 +++ drizzle-orm/src/bigquery-core/db.ts | 371 ++++++++ drizzle-orm/src/bigquery-core/dialect.ts | 845 ++++++++++++++++++ drizzle-orm/src/bigquery-core/index.ts | 12 + drizzle-orm/src/bigquery-core/primary-keys.ts | 63 ++ .../src/bigquery-core/query-builders/count.ts | 78 ++ .../bigquery-core/query-builders/delete.ts | 118 +++ .../src/bigquery-core/query-builders/index.ts | 12 + .../bigquery-core/query-builders/insert.ts | 150 ++++ .../query-builders/query-builder.ts | 112 +++ .../bigquery-core/query-builders/select.ts | 422 +++++++++ .../query-builders/select.types.ts | 405 +++++++++ .../bigquery-core/query-builders/update.ts | 163 ++++ drizzle-orm/src/bigquery-core/session.ts | 144 +++ drizzle-orm/src/bigquery-core/subquery.ts | 25 + drizzle-orm/src/bigquery-core/table.ts | 176 ++++ .../src/bigquery-core/unique-constraint.ts | 65 ++ drizzle-orm/src/bigquery-core/view-base.ts | 14 + drizzle-orm/src/bigquery/driver.ts | 130 +++ drizzle-orm/src/bigquery/index.ts | 2 + drizzle-orm/src/bigquery/migrator.ts | 11 + drizzle-orm/src/bigquery/session.ts | 217 +++++ drizzle-orm/src/column-builder.ts | 9 +- integration-tests/package.json | 3 +- .../tests/bigquery/bigquery-common.ts | 548 ++++++++++++ .../tests/bigquery/bigquery.test.ts | 143 +++ integration-tests/vitest.config.ts | 1 + pnpm-lock.yaml | 694 ++++++++++---- 44 files changed, 5915 insertions(+), 173 deletions(-) create mode 100644 drizzle-orm/src/bigquery-core/alias.ts create mode 100644 drizzle-orm/src/bigquery-core/checks.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/all.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/bool.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/bytes.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/common.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/date.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/datetime.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/float64.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/index.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/int64.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/json.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/numeric.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/string.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/time.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/timestamp.ts create mode 100644 drizzle-orm/src/bigquery-core/db.ts create mode 100644 drizzle-orm/src/bigquery-core/dialect.ts create mode 100644 drizzle-orm/src/bigquery-core/index.ts create mode 100644 drizzle-orm/src/bigquery-core/primary-keys.ts create mode 100644 drizzle-orm/src/bigquery-core/query-builders/count.ts create mode 100644 drizzle-orm/src/bigquery-core/query-builders/delete.ts create mode 100644 drizzle-orm/src/bigquery-core/query-builders/index.ts create mode 100644 drizzle-orm/src/bigquery-core/query-builders/insert.ts create mode 100644 drizzle-orm/src/bigquery-core/query-builders/query-builder.ts create mode 100644 drizzle-orm/src/bigquery-core/query-builders/select.ts create mode 100644 drizzle-orm/src/bigquery-core/query-builders/select.types.ts create mode 100644 drizzle-orm/src/bigquery-core/query-builders/update.ts create mode 100644 drizzle-orm/src/bigquery-core/session.ts create mode 100644 drizzle-orm/src/bigquery-core/subquery.ts create mode 100644 drizzle-orm/src/bigquery-core/table.ts create mode 100644 drizzle-orm/src/bigquery-core/unique-constraint.ts create mode 100644 drizzle-orm/src/bigquery-core/view-base.ts create mode 100644 drizzle-orm/src/bigquery/driver.ts create mode 100644 drizzle-orm/src/bigquery/index.ts create mode 100644 drizzle-orm/src/bigquery/migrator.ts create mode 100644 drizzle-orm/src/bigquery/session.ts create mode 100644 integration-tests/tests/bigquery/bigquery-common.ts create mode 100644 integration-tests/tests/bigquery/bigquery.test.ts diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index 670b87a48f..65c45e08a0 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -59,20 +59,20 @@ "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", + "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", - "sqlite3": ">=5", - "gel": ">=2", - "@upstash/redis": ">=1.34.7" + "sqlite3": ">=5" }, "peerDependenciesMeta": { "mysql2": { @@ -167,6 +167,7 @@ "@aws-sdk/client-rds-data": "^3.549.0", "@cloudflare/workers-types": "^4.20241112.0", "@electric-sql/pglite": "^0.2.12", + "@google-cloud/bigquery": "^8.1.1", "@libsql/client": "^0.10.0", "@libsql/client-wasm": "^0.10.0", "@miniflare/d1": "^2.14.4", diff --git a/drizzle-orm/src/bigquery-core/alias.ts b/drizzle-orm/src/bigquery-core/alias.ts new file mode 100644 index 0000000000..b875cac835 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/alias.ts @@ -0,0 +1,12 @@ +import { TableAliasProxyHandler } from '~/alias.ts'; +import type { BuildAliasTable } from './query-builders/select.types.ts'; + +import type { BigQueryTable } from './table.ts'; +import type { BigQueryViewBase } 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/bigquery-core/checks.ts b/drizzle-orm/src/bigquery-core/checks.ts new file mode 100644 index 0000000000..70cbc666b4 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/checks.ts @@ -0,0 +1,32 @@ +import { entityKind } from '~/entity.ts'; +import type { SQL } from '~/sql/sql.ts'; +import type { BigQueryTable } from './table.ts'; + +export class CheckBuilder { + static readonly [entityKind]: string = 'BigQueryCheckBuilder'; + + protected brand!: 'BigQueryConstraintBuilder'; + + constructor(public name: string, public value: SQL) {} + + /** @internal */ + build(table: BigQueryTable): Check { + return new Check(table, this); + } +} + +export class Check { + static readonly [entityKind]: string = 'BigQueryCheck'; + + readonly name: string; + readonly value: SQL; + + constructor(public table: BigQueryTable, 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/bigquery-core/columns/all.ts b/drizzle-orm/src/bigquery-core/columns/all.ts new file mode 100644 index 0000000000..be58d129a3 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/all.ts @@ -0,0 +1,30 @@ +import { bool } from './bool.ts'; +import { bytes } from './bytes.ts'; +import { date } from './date.ts'; +import { datetime } from './datetime.ts'; +import { float64 } from './float64.ts'; +import { int64 } from './int64.ts'; +import { json } from './json.ts'; +import { bignumeric, numeric } from './numeric.ts'; +import { string } from './string.ts'; +import { time } from './time.ts'; +import { timestamp } from './timestamp.ts'; + +export function getBigQueryColumnBuilders() { + return { + bool, + bytes, + date, + datetime, + float64, + int64, + json, + numeric, + bignumeric, + string, + time, + timestamp, + }; +} + +export type BigQueryColumnBuilders = ReturnType; diff --git a/drizzle-orm/src/bigquery-core/columns/bool.ts b/drizzle-orm/src/bigquery-core/columns/bool.ts new file mode 100644 index 0000000000..9c4f6e0c2d --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/bool.ts @@ -0,0 +1,48 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +export type BigQueryBoolBuilderInitial = BigQueryBoolBuilder<{ + name: TName; + dataType: 'boolean'; + columnType: 'BigQueryBool'; + data: boolean; + driverParam: boolean; + enumValues: undefined; +}>; + +export class BigQueryBoolBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryBoolBuilder'; + + constructor(name: T['name']) { + super(name, 'boolean', 'BigQueryBool'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryBool> { + return new BigQueryBool>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryBool> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryBool'; + + getSQLType(): string { + return 'BOOL'; + } +} + +export function bool(): BigQueryBoolBuilderInitial<''>; +export function bool(name: TName): BigQueryBoolBuilderInitial; +export function bool(name?: string) { + return new BigQueryBoolBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/bigquery-core/columns/bytes.ts b/drizzle-orm/src/bigquery-core/columns/bytes.ts new file mode 100644 index 0000000000..2c3c3726b3 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/bytes.ts @@ -0,0 +1,81 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +export type BigQueryBytesBuilderInitial = BigQueryBytesBuilder<{ + name: TName; + dataType: 'buffer'; + columnType: 'BigQueryBytes'; + data: Buffer; + driverParam: Buffer | Uint8Array | string; + enumValues: undefined; +}>; + +export class BigQueryBytesBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryBytesBuilder'; + + constructor(name: T['name'], length?: number) { + super(name, 'buffer', 'BigQueryBytes'); + this.config.length = length; + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryBytes> { + return new BigQueryBytes>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryBytes> + extends BigQueryColumn +{ + static override readonly [entityKind]: string = 'BigQueryBytes'; + + readonly length: number | undefined = this.config.length; + + getSQLType(): string { + return this.length === undefined ? 'BYTES' : `BYTES(${this.length})`; + } + + override mapFromDriverValue(value: string | Buffer | Uint8Array): Buffer { + if (typeof value === 'string') { + // BigQuery returns base64-encoded bytes + return Buffer.from(value, 'base64'); + } + if (value instanceof Uint8Array) { + return Buffer.from(value); + } + return value; + } + + override mapToDriverValue(value: Buffer | Uint8Array): string { + // Convert to base64 for BigQuery + return Buffer.from(value).toString('base64'); + } +} + +export interface BigQueryBytesConfig { + length?: number; +} + +export function bytes(): BigQueryBytesBuilderInitial<''>; +export function bytes(name: TName): BigQueryBytesBuilderInitial; +export function bytes(config: BigQueryBytesConfig): BigQueryBytesBuilderInitial<''>; +export function bytes( + name: TName, + config: BigQueryBytesConfig, +): BigQueryBytesBuilderInitial; +export function bytes(a?: string | BigQueryBytesConfig, b?: BigQueryBytesConfig) { + if (typeof a === 'object') { + return new BigQueryBytesBuilder('', a.length); + } + return new BigQueryBytesBuilder(a ?? '', b?.length); +} diff --git a/drizzle-orm/src/bigquery-core/columns/common.ts b/drizzle-orm/src/bigquery-core/columns/common.ts new file mode 100644 index 0000000000..d1180fb82e --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/common.ts @@ -0,0 +1,85 @@ +import type { AnyBigQueryTable, BigQueryTable } from '~/bigquery-core/table.ts'; +import { ColumnBuilder } from '~/column-builder.ts'; +import type { + ColumnBuilderBase, + ColumnBuilderBaseConfig, + ColumnBuilderExtraConfig, + ColumnBuilderRuntimeConfig, + ColumnDataType, + HasGenerated, + MakeColumnConfig, +} from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { Column } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { SQL } from '~/sql/sql.ts'; +import type { Update } from '~/utils.ts'; +import { uniqueKeyName } from '../unique-constraint.ts'; + +export interface BigQueryColumnBuilderBase< + T extends ColumnBuilderBaseConfig = ColumnBuilderBaseConfig, + TTypeConfig extends object = object, +> extends ColumnBuilderBase {} + +export interface BigQueryGeneratedColumnConfig { + mode?: 'stored'; +} + +export abstract class BigQueryColumnBuilder< + T extends ColumnBuilderBaseConfig = ColumnBuilderBaseConfig & { + data: any; + }, + TRuntimeConfig extends object = object, + TTypeConfig extends object = object, + TExtraConfig extends ColumnBuilderExtraConfig = ColumnBuilderExtraConfig, +> extends ColumnBuilder + implements BigQueryColumnBuilderBase +{ + static override readonly [entityKind]: string = 'BigQueryColumnBuilder'; + + // BigQuery doesn't enforce foreign keys, but we keep the pattern for schema documentation + unique(name?: string): this { + this.config.isUnique = true; + this.config.uniqueName = name; + return this; + } + + generatedAlwaysAs(as: SQL | T['data'] | (() => SQL), config?: BigQueryGeneratedColumnConfig): HasGenerated { + this.config.generated = { + as, + type: 'always', + mode: config?.mode ?? 'stored', + }; + return this as any; + } + + /** @internal */ + abstract build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryColumn>; +} + +// To understand how to use `BigQueryColumn` and `AnyBigQueryColumn`, see `Column` and `AnyColumn` documentation. +export abstract class BigQueryColumn< + T extends ColumnBaseConfig = ColumnBaseConfig, + TRuntimeConfig extends object = {}, + TTypeConfig extends object = {}, +> extends Column { + static override readonly [entityKind]: string = 'BigQueryColumn'; + + constructor( + override readonly table: BigQueryTable, + config: ColumnBuilderRuntimeConfig, + ) { + if (!config.uniqueName) { + config.uniqueName = uniqueKeyName(table, [config.name]); + } + super(table, config); + } +} + +export type AnyBigQueryColumn> = {}> = BigQueryColumn< + Required, TPartial>> +>; diff --git a/drizzle-orm/src/bigquery-core/columns/date.ts b/drizzle-orm/src/bigquery-core/columns/date.ts new file mode 100644 index 0000000000..412430acf8 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/date.ts @@ -0,0 +1,115 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +export type BigQueryDateBuilderInitial = BigQueryDateBuilder<{ + name: TName; + dataType: 'date'; + columnType: 'BigQueryDate'; + data: Date; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryDateBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryDateBuilder'; + + constructor(name: T['name']) { + super(name, 'date', 'BigQueryDate'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryDate> { + return new BigQueryDate>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryDate> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryDate'; + + getSQLType(): string { + return 'DATE'; + } + + override mapFromDriverValue(value: string | { value: string }): Date { + const strValue = typeof value === 'object' && 'value' in value ? value.value : value; + return new Date(strValue); + } + + override mapToDriverValue(value: Date): string { + return value.toISOString().split('T')[0]!; + } +} + +// String mode for date +export type BigQueryDateStringBuilderInitial = BigQueryDateStringBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryDateString'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryDateStringBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryDateStringBuilder'; + + constructor(name: T['name']) { + super(name, 'string', 'BigQueryDateString'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryDateString> { + return new BigQueryDateString>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryDateString> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryDateString'; + + getSQLType(): string { + return 'DATE'; + } + + override mapFromDriverValue(value: string | { value: string }): string { + return typeof value === 'object' && 'value' in value ? value.value : value; + } +} + +export interface BigQueryDateConfig { + mode: TMode; +} + +export function date( + config: BigQueryDateConfig, +): TMode extends 'string' ? BigQueryDateStringBuilderInitial<''> : BigQueryDateBuilderInitial<''>; +export function date( + name: TName, + config: BigQueryDateConfig, +): TMode extends 'string' ? BigQueryDateStringBuilderInitial : BigQueryDateBuilderInitial; +export function date(): BigQueryDateBuilderInitial<''>; +export function date(name: TName): BigQueryDateBuilderInitial; +export function date(a?: string | BigQueryDateConfig, b?: BigQueryDateConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'string') { + return new BigQueryDateStringBuilder(name); + } + return new BigQueryDateBuilder(name); +} diff --git a/drizzle-orm/src/bigquery-core/columns/datetime.ts b/drizzle-orm/src/bigquery-core/columns/datetime.ts new file mode 100644 index 0000000000..40afac7a95 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/datetime.ts @@ -0,0 +1,119 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +// DATETIME as Date (note: DATETIME has no timezone in BigQuery) +export type BigQueryDatetimeBuilderInitial = BigQueryDatetimeBuilder<{ + name: TName; + dataType: 'date'; + columnType: 'BigQueryDatetime'; + data: Date; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryDatetimeBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryDatetimeBuilder'; + + constructor(name: T['name']) { + super(name, 'date', 'BigQueryDatetime'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryDatetime> { + return new BigQueryDatetime>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryDatetime> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryDatetime'; + + getSQLType(): string { + return 'DATETIME'; + } + + override mapFromDriverValue(value: string | { value: string }): Date { + const strValue = typeof value === 'object' && 'value' in value ? value.value : value; + return new Date(strValue); + } + + override mapToDriverValue(value: Date): string { + // DATETIME format: YYYY-MM-DD HH:MM:SS[.SSSSSS] + return value.toISOString().replace('T', ' ').replace('Z', ''); + } +} + +// String mode for datetime +export type BigQueryDatetimeStringBuilderInitial = BigQueryDatetimeStringBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryDatetimeString'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryDatetimeStringBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryDatetimeStringBuilder'; + + constructor(name: T['name']) { + super(name, 'string', 'BigQueryDatetimeString'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryDatetimeString> { + return new BigQueryDatetimeString>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryDatetimeString> + extends BigQueryColumn +{ + static override readonly [entityKind]: string = 'BigQueryDatetimeString'; + + getSQLType(): string { + return 'DATETIME'; + } + + override mapFromDriverValue(value: string | { value: string }): string { + return typeof value === 'object' && 'value' in value ? value.value : value; + } +} + +export interface BigQueryDatetimeConfig { + mode: TMode; +} + +export function datetime( + config: BigQueryDatetimeConfig, +): TMode extends 'string' ? BigQueryDatetimeStringBuilderInitial<''> : BigQueryDatetimeBuilderInitial<''>; +export function datetime( + name: TName, + config: BigQueryDatetimeConfig, +): TMode extends 'string' ? BigQueryDatetimeStringBuilderInitial : BigQueryDatetimeBuilderInitial; +export function datetime(): BigQueryDatetimeBuilderInitial<''>; +export function datetime(name: TName): BigQueryDatetimeBuilderInitial; +export function datetime(a?: string | BigQueryDatetimeConfig, b?: BigQueryDatetimeConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'string') { + return new BigQueryDatetimeStringBuilder(name); + } + return new BigQueryDatetimeBuilder(name); +} diff --git a/drizzle-orm/src/bigquery-core/columns/float64.ts b/drizzle-orm/src/bigquery-core/columns/float64.ts new file mode 100644 index 0000000000..d42c6c1aa9 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/float64.ts @@ -0,0 +1,48 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +export type BigQueryFloat64BuilderInitial = BigQueryFloat64Builder<{ + name: TName; + dataType: 'number'; + columnType: 'BigQueryFloat64'; + data: number; + driverParam: number; + enumValues: undefined; +}>; + +export class BigQueryFloat64Builder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryFloat64Builder'; + + constructor(name: T['name']) { + super(name, 'number', 'BigQueryFloat64'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryFloat64> { + return new BigQueryFloat64>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryFloat64> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryFloat64'; + + getSQLType(): string { + return 'FLOAT64'; + } +} + +export function float64(): BigQueryFloat64BuilderInitial<''>; +export function float64(name: TName): BigQueryFloat64BuilderInitial; +export function float64(name?: string) { + return new BigQueryFloat64Builder(name ?? ''); +} diff --git a/drizzle-orm/src/bigquery-core/columns/index.ts b/drizzle-orm/src/bigquery-core/columns/index.ts new file mode 100644 index 0000000000..fbe0554456 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/index.ts @@ -0,0 +1,13 @@ +export * from './all.ts'; +export * from './bool.ts'; +export * from './bytes.ts'; +export * from './common.ts'; +export * from './date.ts'; +export * from './datetime.ts'; +export * from './float64.ts'; +export * from './int64.ts'; +export * from './json.ts'; +export * from './numeric.ts'; +export * from './string.ts'; +export * from './time.ts'; +export * from './timestamp.ts'; diff --git a/drizzle-orm/src/bigquery-core/columns/int64.ts b/drizzle-orm/src/bigquery-core/columns/int64.ts new file mode 100644 index 0000000000..0275458d0d --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/int64.ts @@ -0,0 +1,118 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +// INT64 as number (safe for values within Number.MAX_SAFE_INTEGER) +export type BigQueryInt64NumberBuilderInitial = BigQueryInt64NumberBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'BigQueryInt64Number'; + data: number; + driverParam: number | string; + enumValues: undefined; +}>; + +export class BigQueryInt64NumberBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryInt64NumberBuilder'; + + constructor(name: T['name']) { + super(name, 'number', 'BigQueryInt64Number'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryInt64Number> { + return new BigQueryInt64Number>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryInt64Number> + extends BigQueryColumn +{ + static override readonly [entityKind]: string = 'BigQueryInt64Number'; + + getSQLType(): string { + return 'INT64'; + } + + override mapFromDriverValue(value: number | string): number { + if (typeof value === 'number') { + return value; + } + return Number(value); + } +} + +// INT64 as bigint (for full 64-bit range) +export type BigQueryInt64BigIntBuilderInitial = BigQueryInt64BigIntBuilder<{ + name: TName; + dataType: 'bigint'; + columnType: 'BigQueryInt64BigInt'; + data: bigint; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryInt64BigIntBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryInt64BigIntBuilder'; + + constructor(name: T['name']) { + super(name, 'bigint', 'BigQueryInt64BigInt'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryInt64BigInt> { + return new BigQueryInt64BigInt>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryInt64BigInt> + extends BigQueryColumn +{ + static override readonly [entityKind]: string = 'BigQueryInt64BigInt'; + + getSQLType(): string { + return 'INT64'; + } + + override mapFromDriverValue(value: string): bigint { + return BigInt(value); + } +} + +export interface BigQueryInt64Config { + mode: T; +} + +export function int64( + config: BigQueryInt64Config, +): TMode extends 'number' ? BigQueryInt64NumberBuilderInitial<''> : BigQueryInt64BigIntBuilderInitial<''>; +export function int64( + name: TName, + config: BigQueryInt64Config, +): TMode extends 'number' ? BigQueryInt64NumberBuilderInitial : BigQueryInt64BigIntBuilderInitial; +export function int64(): BigQueryInt64NumberBuilderInitial<''>; +export function int64(name: TName): BigQueryInt64NumberBuilderInitial; +export function int64(a?: string | BigQueryInt64Config, b?: BigQueryInt64Config) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'bigint') { + return new BigQueryInt64BigIntBuilder(name); + } + return new BigQueryInt64NumberBuilder(name); +} diff --git a/drizzle-orm/src/bigquery-core/columns/json.ts b/drizzle-orm/src/bigquery-core/columns/json.ts new file mode 100644 index 0000000000..8bb2bb704c --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/json.ts @@ -0,0 +1,59 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +export type BigQueryJsonBuilderInitial = BigQueryJsonBuilder<{ + name: TName; + dataType: 'json'; + columnType: 'BigQueryJson'; + data: unknown; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryJsonBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryJsonBuilder'; + + constructor(name: T['name']) { + super(name, 'json', 'BigQueryJson'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryJson> { + return new BigQueryJson>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryJson> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryJson'; + + getSQLType(): string { + return 'JSON'; + } + + override mapFromDriverValue(value: string | object): unknown { + if (typeof value === 'string') { + return JSON.parse(value); + } + return value; + } + + override mapToDriverValue(value: unknown): string { + return JSON.stringify(value); + } +} + +export function json(): BigQueryJsonBuilderInitial<''>; +export function json(name: TName): BigQueryJsonBuilderInitial; +export function json(name?: string) { + return new BigQueryJsonBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/bigquery-core/columns/numeric.ts b/drizzle-orm/src/bigquery-core/columns/numeric.ts new file mode 100644 index 0000000000..1326ccdb26 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/numeric.ts @@ -0,0 +1,152 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +// NUMERIC - precision up to 38 digits, scale up to 9 digits +// Stored as string to preserve precision +export type BigQueryNumericBuilderInitial = BigQueryNumericBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryNumeric'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryNumericBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryNumericBuilder'; + + constructor(name: T['name'], precision?: number, scale?: number) { + super(name, 'string', 'BigQueryNumeric'); + this.config.precision = precision; + this.config.scale = scale; + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryNumeric> { + return new BigQueryNumeric>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryNumeric> + extends BigQueryColumn +{ + static override readonly [entityKind]: string = 'BigQueryNumeric'; + + readonly precision: number | undefined = this.config.precision; + readonly scale: number | undefined = this.config.scale; + + getSQLType(): string { + if (this.precision !== undefined && this.scale !== undefined) { + return `NUMERIC(${this.precision}, ${this.scale})`; + } + if (this.precision !== undefined) { + return `NUMERIC(${this.precision})`; + } + return 'NUMERIC'; + } + + override mapFromDriverValue(value: string | number | { value: string }): string { + if (typeof value === 'object' && 'value' in value) { + return value.value; + } + return String(value); + } +} + +export interface BigQueryNumericConfig { + precision?: number; + scale?: number; +} + +export function numeric(): BigQueryNumericBuilderInitial<''>; +export function numeric(name: TName): BigQueryNumericBuilderInitial; +export function numeric(config: BigQueryNumericConfig): BigQueryNumericBuilderInitial<''>; +export function numeric( + name: TName, + config: BigQueryNumericConfig, +): BigQueryNumericBuilderInitial; +export function numeric(a?: string | BigQueryNumericConfig, b?: BigQueryNumericConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new BigQueryNumericBuilder(name, config?.precision, config?.scale); +} + +// BIGNUMERIC - precision up to 76.76 digits (38.38 before and after decimal) +export type BigQueryBigNumericBuilderInitial = BigQueryBigNumericBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryBigNumeric'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryBigNumericBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryBigNumericBuilder'; + + constructor(name: T['name'], precision?: number, scale?: number) { + super(name, 'string', 'BigQueryBigNumeric'); + this.config.precision = precision; + this.config.scale = scale; + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryBigNumeric> { + return new BigQueryBigNumeric>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryBigNumeric> + extends BigQueryColumn +{ + static override readonly [entityKind]: string = 'BigQueryBigNumeric'; + + readonly precision: number | undefined = this.config.precision; + readonly scale: number | undefined = this.config.scale; + + getSQLType(): string { + if (this.precision !== undefined && this.scale !== undefined) { + return `BIGNUMERIC(${this.precision}, ${this.scale})`; + } + if (this.precision !== undefined) { + return `BIGNUMERIC(${this.precision})`; + } + return 'BIGNUMERIC'; + } + + override mapFromDriverValue(value: string | number | { value: string }): string { + if (typeof value === 'object' && 'value' in value) { + return value.value; + } + return String(value); + } +} + +export function bignumeric(): BigQueryBigNumericBuilderInitial<''>; +export function bignumeric(name: TName): BigQueryBigNumericBuilderInitial; +export function bignumeric(config: BigQueryNumericConfig): BigQueryBigNumericBuilderInitial<''>; +export function bignumeric( + name: TName, + config: BigQueryNumericConfig, +): BigQueryBigNumericBuilderInitial; +export function bignumeric(a?: string | BigQueryNumericConfig, b?: BigQueryNumericConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + return new BigQueryBigNumericBuilder(name, config?.precision, config?.scale); +} diff --git a/drizzle-orm/src/bigquery-core/columns/string.ts b/drizzle-orm/src/bigquery-core/columns/string.ts new file mode 100644 index 0000000000..dfe00547a0 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/string.ts @@ -0,0 +1,65 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +export type BigQueryStringBuilderInitial = BigQueryStringBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryString'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryStringBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryStringBuilder'; + + constructor(name: T['name'], length?: number) { + super(name, 'string', 'BigQueryString'); + this.config.length = length; + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryString> { + return new BigQueryString>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryString> + extends BigQueryColumn +{ + static override readonly [entityKind]: string = 'BigQueryString'; + + readonly length: number | undefined = this.config.length; + + getSQLType(): string { + return this.length === undefined ? 'STRING' : `STRING(${this.length})`; + } +} + +export interface BigQueryStringConfig { + length?: number; +} + +export function string(): BigQueryStringBuilderInitial<''>; +export function string(name: TName): BigQueryStringBuilderInitial; +export function string(config: BigQueryStringConfig): BigQueryStringBuilderInitial<''>; +export function string( + name: TName, + config: BigQueryStringConfig, +): BigQueryStringBuilderInitial; +export function string(a?: string | BigQueryStringConfig, b?: BigQueryStringConfig) { + if (typeof a === 'object') { + return new BigQueryStringBuilder('', a.length); + } + return new BigQueryStringBuilder(a ?? '', b?.length); +} diff --git a/drizzle-orm/src/bigquery-core/columns/time.ts b/drizzle-orm/src/bigquery-core/columns/time.ts new file mode 100644 index 0000000000..bf24b9e78e --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/time.ts @@ -0,0 +1,53 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +// TIME is always returned as a string (HH:MM:SS[.SSSSSS]) +export type BigQueryTimeBuilderInitial = BigQueryTimeBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryTime'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryTimeBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryTimeBuilder'; + + constructor(name: T['name']) { + super(name, 'string', 'BigQueryTime'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryTime> { + return new BigQueryTime>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryTime> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryTime'; + + getSQLType(): string { + return 'TIME'; + } + + override mapFromDriverValue(value: string | { value: string }): string { + return typeof value === 'object' && 'value' in value ? value.value : value; + } +} + +export function time(): BigQueryTimeBuilderInitial<''>; +export function time(name: TName): BigQueryTimeBuilderInitial; +export function time(name?: string) { + return new BigQueryTimeBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/bigquery-core/columns/timestamp.ts b/drizzle-orm/src/bigquery-core/columns/timestamp.ts new file mode 100644 index 0000000000..e373c7edb5 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/timestamp.ts @@ -0,0 +1,118 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +export type BigQueryTimestampBuilderInitial = BigQueryTimestampBuilder<{ + name: TName; + dataType: 'date'; + columnType: 'BigQueryTimestamp'; + data: Date; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryTimestampBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryTimestampBuilder'; + + constructor(name: T['name']) { + super(name, 'date', 'BigQueryTimestamp'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryTimestamp> { + return new BigQueryTimestamp>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryTimestamp> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryTimestamp'; + + getSQLType(): string { + return 'TIMESTAMP'; + } + + override mapFromDriverValue(value: string | { value: string }): Date { + // BigQuery returns timestamps as ISO strings or objects + const strValue = typeof value === 'object' && 'value' in value ? value.value : value; + return new Date(strValue); + } + + override mapToDriverValue(value: Date): string { + return value.toISOString(); + } +} + +// String mode for timestamp (returns raw string) +export type BigQueryTimestampStringBuilderInitial = BigQueryTimestampStringBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryTimestampString'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryTimestampStringBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryTimestampStringBuilder'; + + constructor(name: T['name']) { + super(name, 'string', 'BigQueryTimestampString'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryTimestampString> { + return new BigQueryTimestampString>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryTimestampString> + extends BigQueryColumn +{ + static override readonly [entityKind]: string = 'BigQueryTimestampString'; + + getSQLType(): string { + return 'TIMESTAMP'; + } + + override mapFromDriverValue(value: string | { value: string }): string { + return typeof value === 'object' && 'value' in value ? value.value : value; + } +} + +export interface BigQueryTimestampConfig { + mode: TMode; +} + +export function timestamp( + config: BigQueryTimestampConfig, +): TMode extends 'string' ? BigQueryTimestampStringBuilderInitial<''> : BigQueryTimestampBuilderInitial<''>; +export function timestamp( + name: TName, + config: BigQueryTimestampConfig, +): TMode extends 'string' ? BigQueryTimestampStringBuilderInitial : BigQueryTimestampBuilderInitial; +export function timestamp(): BigQueryTimestampBuilderInitial<''>; +export function timestamp(name: TName): BigQueryTimestampBuilderInitial; +export function timestamp(a?: string | BigQueryTimestampConfig, b?: BigQueryTimestampConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'string') { + return new BigQueryTimestampStringBuilder(name); + } + return new BigQueryTimestampBuilder(name); +} diff --git a/drizzle-orm/src/bigquery-core/db.ts b/drizzle-orm/src/bigquery-core/db.ts new file mode 100644 index 0000000000..9d466c621c --- /dev/null +++ b/drizzle-orm/src/bigquery-core/db.ts @@ -0,0 +1,371 @@ +import type { BigQueryDialect } from '~/bigquery-core/dialect.ts'; +import { + BigQueryCountBuilder, + BigQueryDeleteBase, + BigQueryInsertBuilder, + BigQuerySelectBuilder, + BigQueryUpdateBuilder, +} from '~/bigquery-core/query-builders/index.ts'; +import type { + BigQueryQueryResultHKT, + BigQueryQueryResultKind, + BigQuerySession, + BigQueryTransaction, + BigQueryTransactionConfig, + PreparedQueryConfig, +} from '~/bigquery-core/session.ts'; +import type { BigQueryTable } from '~/bigquery-core/table.ts'; +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 { BigQueryColumn } from './columns/index.ts'; +import type { SelectedFields } from './query-builders/select.types.ts'; + +export class BigQueryDatabase< + TQueryResult extends BigQueryQueryResultHKT, + TFullSchema extends Record = Record, + TSchema extends TablesRelationalConfig = ExtractTablesWithRelations, +> { + static readonly [entityKind]: string = 'BigQueryDatabase'; + + declare readonly _: { + readonly schema: TSchema | undefined; + readonly fullSchema: TFullSchema; + readonly tableNamesMap: Record; + readonly session: BigQuerySession; + }; + + constructor( + /** @internal */ + readonly dialect: BigQueryDialect, + /** @internal */ + readonly session: BigQuerySession, + schema: RelationalSchemaConfig | undefined, + ) { + this._ = schema + ? { + schema: schema.schema, + fullSchema: schema.fullSchema as TFullSchema, + tableNamesMap: schema.tableNamesMap, + session, + } + : { + schema: undefined, + fullSchema: {} as TFullSchema, + tableNamesMap: {}, + session, + }; + } + + /** + * Returns the count of rows that match the given filter criteria. + * + * @param source The table or SQL expression to count from. + * @param filters Optional filter conditions. + * + * @example + * + * ```ts + * // Count all rows in the 'users' table + * const count = await db.$count(users); + * + * // Count rows with filters + * const activeCount = await db.$count(users, eq(users.active, true)); + * ``` + */ + $count( + source: BigQueryTable | SQL | SQLWrapper, + filters?: SQL, + ) { + return new BigQueryCountBuilder({ source, filters, session: this.session }); + } + + /** + * 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. + * + * @param alias The alias for the subquery. + * + * @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); + * ``` + */ + $with(alias: string) { + const self = this; + return { + as( + qb: TypedQueryBuilder | SQL, + ) { + return new Proxy( + new WithSubquery( + qb.getSQL(), + ('getSelectedFields' in qb ? qb.getSelectedFields() ?? {} : {}) as SelectedFields, + alias, + true, + ), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), + ); + }, + }; + } + + /** + * Incorporates a previously defined CTE (using `$with`) into the main query. + * + * @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; + + function select(): BigQuerySelectBuilder; + function select(fields: TSelection): BigQuerySelectBuilder; + function select( + fields?: TSelection, + ): BigQuerySelectBuilder { + return new BigQuerySelectBuilder({ + fields: fields ?? undefined, + session: self.session, + dialect: self.dialect, + withList: queries, + }); + } + + function selectDistinct(): BigQuerySelectBuilder; + function selectDistinct(fields: TSelection): BigQuerySelectBuilder; + function selectDistinct( + fields?: TSelection, + ): BigQuerySelectBuilder { + return new BigQuerySelectBuilder({ + fields: fields ?? undefined, + session: self.session, + dialect: self.dialect, + withList: queries, + distinct: true, + }); + } + + function update(table: TTable): BigQueryUpdateBuilder { + return new BigQueryUpdateBuilder(table, self.session, self.dialect, queries); + } + + function insert(table: TTable): BigQueryInsertBuilder { + return new BigQueryInsertBuilder(table, self.session, self.dialect, queries); + } + + function delete_(table: TTable): BigQueryDeleteBase { + return new BigQueryDeleteBase(table, self.session, self.dialect, queries); + } + + return { select, selectDistinct, update, insert, 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. + * + * @param fields The selection object. + * + * @example + * + * ```ts + * // Select all columns and all rows from the 'users' table + * const allUsers = await db.select().from(users); + * + * // Select specific columns and all rows from the 'users' table + * const usersWithIdAndName = await db.select({ + * id: users.id, + * name: users.name + * }) + * .from(users); + * ``` + */ + select(): BigQuerySelectBuilder; + select(fields: TSelection): BigQuerySelectBuilder; + select(fields?: TSelection): BigQuerySelectBuilder { + return new BigQuerySelectBuilder({ + 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. + * + * @param fields The selection object. + * + * @example + * ```ts + * // Select all unique rows from the 'users' table + * await db.selectDistinct() + * .from(users) + * .orderBy(users.id, users.name); + * + * // Select all unique names from the 'users' table + * await db.selectDistinct({ name: users.name }) + * .from(users) + * .orderBy(users.name); + * ``` + */ + selectDistinct(): BigQuerySelectBuilder; + selectDistinct(fields: TSelection): BigQuerySelectBuilder; + selectDistinct( + fields?: TSelection, + ): BigQuerySelectBuilder { + return new BigQuerySelectBuilder({ + 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. + * + * Note: BigQuery UPDATE does not support RETURNING clause. + * + * @param table The table to update. + * + * @example + * + * ```ts + * // Update all rows in the 'users' table + * await db.update(users).set({ active: true }); + * + * // Update rows with filters and conditions + * await db.update(users).set({ active: true }).where(eq(users.role, 'admin')); + * ``` + */ + update(table: TTable): BigQueryUpdateBuilder { + return new BigQueryUpdateBuilder(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. + * + * Note: BigQuery INSERT does not support RETURNING clause. + * + * @param table The table to insert into. + * + * @example + * + * ```ts + * // Insert one row + * await db.insert(users).values({ name: 'John' }); + * + * // Insert multiple rows + * await db.insert(users).values([{ name: 'John' }, { name: 'Jane' }]); + * ``` + */ + insert(table: TTable): BigQueryInsertBuilder { + return new BigQueryInsertBuilder(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. + * + * Note: BigQuery DELETE does not support RETURNING clause. + * + * @param table The table to delete from. + * + * @example + * + * ```ts + * // Delete all rows in the 'users' table + * await db.delete(users); + * + * // Delete rows with filters and conditions + * await db.delete(users).where(eq(users.active, false)); + * ``` + */ + delete(table: TTable): BigQueryDeleteBase { + return new BigQueryDeleteBase(table, this.session, this.dialect); + } + + /** + * Execute a raw SQL query. + * + * @param query The SQL query to execute. + * + * @example + * + * ```ts + * const result = await db.execute(sql`SELECT * FROM users WHERE id = ${userId}`); + * ``` + */ + execute = Record>( + query: SQLWrapper | string, + ): Promise> { + const sequel = typeof query === 'string' ? sql.raw(query) : query.getSQL(); + const builtQuery = this.dialect.sqlToQuery(sequel); + const prepared = this.session.prepareQuery< + PreparedQueryConfig & { execute: BigQueryQueryResultKind } + >( + builtQuery, + undefined, + undefined, + false, + ); + return prepared.execute(); + } + + /** + * Execute operations within a transaction. + * + * Note: BigQuery has limited transaction support via multi-statement transactions. + * + * @param transaction The transaction function. + * @param config Optional transaction configuration. + * + * @example + * + * ```ts + * await db.transaction(async (tx) => { + * await tx.insert(users).values({ name: 'John' }); + * await tx.update(users).set({ active: true }).where(eq(users.name, 'John')); + * }); + * ``` + */ + transaction( + transaction: (tx: BigQueryTransaction) => Promise, + config?: BigQueryTransactionConfig, + ): Promise { + return this.session.transaction(transaction, config); + } +} diff --git a/drizzle-orm/src/bigquery-core/dialect.ts b/drizzle-orm/src/bigquery-core/dialect.ts new file mode 100644 index 0000000000..c0ebf88f70 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/dialect.ts @@ -0,0 +1,845 @@ +import { aliasedTable, aliasedTableColumn, mapColumnsInAliasedSQLToAlias, mapColumnsInSQLToAlias } from '~/alias.ts'; +import { BigQueryColumn } from '~/bigquery-core/columns/index.ts'; +import type { + BigQueryDeleteConfig, + BigQueryInsertConfig, + BigQuerySelectJoinConfig, + BigQueryUpdateConfig, +} from '~/bigquery-core/query-builders/index.ts'; +import type { BigQuerySelectConfig, SelectedFieldsOrdered } from '~/bigquery-core/query-builders/select.types.ts'; +import { BigQueryTable } from '~/bigquery-core/table.ts'; +import { CasingCache } from '~/casing.ts'; +import { Column } from '~/column.ts'; +import { entityKind, is } from '~/entity.ts'; +import { DrizzleError } from '~/errors.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 { and, eq, View } from '~/sql/index.ts'; +import { + type DriverValueEncoder, + type Name, + Param, + type QueryTypingsValue, + type QueryWithTypings, + SQL, + sql, + type 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 type { BigQuerySession } from './session.ts'; +import { BigQueryViewBase } from './view-base.ts'; + +export interface BigQueryDialectConfig { + casing?: Casing; +} + +export class BigQueryDialect { + static readonly [entityKind]: string = 'BigQueryDialect'; + + /** @internal */ + readonly casing: CasingCache; + + constructor(config?: BigQueryDialectConfig) { + this.casing = new CasingCache(config?.casing); + } + + async migrate( + migrations: MigrationMeta[], + session: BigQuerySession, + config: string | MigrationConfig, + ): Promise { + const migrationsTable = typeof config === 'string' + ? '__drizzle_migrations' + : config.migrationsTable ?? '__drizzle_migrations'; + // BigQuery uses datasets instead of schemas - use migrationsSchema as dataset name + const migrationsDataset = typeof config === 'string' ? 'drizzle' : config.migrationsSchema ?? 'drizzle'; + + // Create migrations dataset if it doesn't exist (BigQuery DDL) + await session.execute(sql.raw(`CREATE SCHEMA IF NOT EXISTS \`${migrationsDataset}\``)); + + // Create migrations table if it doesn't exist + // Note: BigQuery doesn't have SERIAL, using INT64 with manually managed IDs + const migrationTableCreate = sql.raw(` + CREATE TABLE IF NOT EXISTS \`${migrationsDataset}\`.\`${migrationsTable}\` ( + id INT64 NOT NULL, + hash STRING NOT NULL, + created_at INT64 + ) + `); + await session.execute(migrationTableCreate); + + const dbMigrations = await session.all<{ id: number; hash: string; created_at: string }>( + sql.raw( + `SELECT id, hash, created_at FROM \`${migrationsDataset}\`.\`${migrationsTable}\` ORDER BY created_at DESC LIMIT 1`, + ), + ); + + const lastDbMigration = dbMigrations[0]; + + // Get next ID for migrations + const countResult = await session.all<{ cnt: number }>( + sql.raw(`SELECT COUNT(*) as cnt FROM \`${migrationsDataset}\`.\`${migrationsTable}\``), + ); + let nextId = (countResult[0]?.cnt ?? 0) + 1; + + // BigQuery doesn't support traditional transactions for DDL, so we run migrations sequentially + for await (const migration of migrations) { + if ( + !lastDbMigration + || Number(lastDbMigration.created_at) < migration.folderMillis + ) { + for (const stmt of migration.sql) { + if (stmt.trim()) { + await session.execute(sql.raw(stmt)); + } + } + await session.execute( + sql.raw( + `INSERT INTO \`${migrationsDataset}\`.\`${migrationsTable}\` (id, hash, created_at) VALUES (${nextId++}, '${migration.hash}', ${migration.folderMillis})`, + ), + ); + } + } + } + + // BigQuery uses backticks for identifiers (like MySQL) + escapeName(name: string): string { + return `\`${name}\``; + } + + // BigQuery uses positional ? parameters (like MySQL) + 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); + } + + // BigQuery DELETE - no RETURNING clause + buildDeleteQuery({ table, where, withList }: BigQueryDeleteConfig): SQL { + const withSql = this.buildWithCTE(withList); + const whereSql = where ? sql` where ${where}` : undefined; + + return sql`${withSql}delete from ${table}${whereSql}`; + } + + buildUpdateSet(table: BigQueryTable, 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 onUpdateFnResult = col.onUpdateFn?.(); + const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col)); + const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`; + + if (i < setSize - 1) { + return [res, sql.raw(', ')]; + } + return [res]; + })); + } + + // BigQuery UPDATE - no RETURNING clause, no FROM clause support + buildUpdateQuery({ table, set, where, withList }: BigQueryUpdateConfig): SQL { + const withSql = this.buildWithCTE(withList); + + const tableName = table[BigQueryTable.Symbol.Name]; + const tableSchema = table[BigQueryTable.Symbol.Schema]; + const origTableName = table[BigQueryTable.Symbol.OriginalName]; + const alias = tableName === origTableName ? undefined : tableName; + const tableSql = sql`${tableSchema ? sql`${sql.identifier(tableSchema)}.` : undefined}${ + sql.identifier(origTableName) + }${alias && sql` ${sql.identifier(alias)}`}`; + + const setSql = this.buildUpdateSet(table, set); + const whereSql = where ? sql` where ${where}` : undefined; + + return sql`${withSql}update ${tableSql} set ${setSql}${whereSql}`; + } + + /** + * Builds selection SQL with provided fields/expressions + * + * Examples: + * + * `select from` + * + * 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, BigQueryColumn)) { + 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); + } + } else if (is(field, Subquery)) { + const entries = Object.entries(field._.selectedFields) as [string, SQL.Aliased | Column | SQL][]; + + if (entries.length === 1) { + const entry = entries[0]![1]; + + const fieldDecoder = is(entry, SQL) + ? entry.decoder + : is(entry, Column) + ? { mapFromDriverValue: (v: any) => entry.mapFromDriverValue(v) } + : entry.sql.decoder; + + if (fieldDecoder) { + field._.sql.decoder = fieldDecoder; + } + } + chunk.push(field); + } + + if (i < columnsLen - 1) { + chunk.push(sql`, `); + } + + return chunk; + }); + + return sql.join(chunks); + } + + private buildJoins(joins: BigQuerySelectJoinConfig[] | undefined): SQL | undefined { + if (!joins || joins.length === 0) { + return undefined; + } + + const joinsArray: SQL[] = []; + + for (const [index, joinMeta] of joins.entries()) { + if (index === 0) { + joinsArray.push(sql` `); + } + const table = joinMeta.table; + const onSql = joinMeta.on ? sql` on ${joinMeta.on}` : undefined; + + if (is(table, BigQueryTable)) { + const tableName = table[BigQueryTable.Symbol.Name]; + const tableSchema = table[BigQueryTable.Symbol.Schema]; + const origTableName = table[BigQueryTable.Symbol.OriginalName]; + const alias = tableName === origTableName ? undefined : joinMeta.alias; + joinsArray.push( + sql`${sql.raw(joinMeta.joinType)} join ${tableSchema ? sql`${sql.identifier(tableSchema)}.` : undefined}${ + sql.identifier(origTableName) + }${alias && sql` ${sql.identifier(alias)}`}${onSql}`, + ); + } 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 ${viewSchema ? sql`${sql.identifier(viewSchema)}.` : undefined}${ + sql.identifier(origViewName) + }${alias && sql` ${sql.identifier(alias)}`}${onSql}`, + ); + } else { + joinsArray.push( + sql`${sql.raw(joinMeta.joinType)} join ${table}${onSql}`, + ); + } + if (index < joins.length - 1) { + joinsArray.push(sql` `); + } + } + + return sql.join(joinsArray); + } + + private buildFromTable( + table: SQL | Subquery | BigQueryViewBase | BigQueryTable | undefined, + ): SQL | Subquery | BigQueryViewBase | BigQueryTable | undefined { + if (is(table, Table) && table[Table.Symbol.IsAlias]) { + let fullName = sql`${sql.identifier(table[Table.Symbol.OriginalName])}`; + if (table[Table.Symbol.Schema]) { + fullName = sql`${sql.identifier(table[Table.Symbol.Schema]!)}.${fullName}`; + } + return sql`${fullName} ${sql.identifier(table[Table.Symbol.Name])}`; + } + + return table; + } + + buildSelectQuery( + { + withList, + fields, + fieldsFlat, + where, + having, + table, + joins, + orderBy, + groupBy, + limit, + offset, + distinct, + setOperators, + }: BigQuerySelectConfig, + ): 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, BigQueryViewBase) + ? 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); + + // BigQuery only supports DISTINCT, not DISTINCT ON + let distinctSql: SQL | undefined; + if (distinct) { + distinctSql = sql` distinct`; + } + + const selection = this.buildSelection(fieldsList, { isSingleTable }); + + const tableSql = this.buildFromTable(table); + + const joinsSql = this.buildJoins(joins); + + const whereSql = where ? sql` where ${where}` : undefined; + + const havingSql = having ? sql` having ${having}` : undefined; + + let orderBySql; + if (orderBy && orderBy.length > 0) { + orderBySql = sql` order by ${sql.join(orderBy, sql`, `)}`; + } + + let groupBySql; + if (groupBy && groupBy.length > 0) { + groupBySql = sql` group by ${sql.join(groupBy, sql`, `)}`; + } + + const limitSql = typeof limit === 'object' || (typeof limit === 'number' && limit >= 0) + ? sql` limit ${limit}` + : undefined; + + const offsetSql = offset ? sql` offset ${offset}` : undefined; + + // BigQuery doesn't support FOR UPDATE/SHARE locking + const finalQuery = + sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}`; + + if (setOperators.length > 0) { + return this.buildSetOperations(finalQuery, setOperators); + } + + return finalQuery; + } + + buildSetOperations(leftSelect: SQL, setOperators: BigQuerySelectConfig['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 }); + } + + return this.buildSetOperations( + this.buildSetOperationQuery({ leftSelect, setOperator }), + rest, + ); + } + + buildSetOperationQuery({ + leftSelect, + setOperator: { type, isAll, rightSelect, limit, orderBy, offset }, + }: { leftSelect: SQL; setOperator: BigQuerySelectConfig['setOperators'][number] }): SQL { + const leftChunk = sql`(${leftSelect.getSQL()}) `; + const rightChunk = sql`(${rightSelect.getSQL()})`; + + let orderBySql; + if (orderBy && orderBy.length > 0) { + const orderByValues: (SQL | Name)[] = []; + + for (const singleOrderBy of orderBy) { + if (is(singleOrderBy, BigQueryColumn)) { + orderByValues.push(sql.identifier(singleOrderBy.name)); + } else if (is(singleOrderBy, SQL)) { + for (let i = 0; i < singleOrderBy.queryChunks.length; i++) { + const chunk = singleOrderBy.queryChunks[i]; + + if (is(chunk, BigQueryColumn)) { + singleOrderBy.queryChunks[i] = sql.identifier(chunk.name); + } + } + + orderByValues.push(sql`${singleOrderBy}`); + } else { + orderByValues.push(sql`${singleOrderBy}`); + } + } + + 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}`; + } + + // BigQuery INSERT - no RETURNING clause, no ON CONFLICT + buildInsertQuery( + { table, values, withList, select }: BigQueryInsertConfig, + ): SQL { + const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; + const columns: Record = table[Table.Symbol.Columns]; + + const colEntries: [string, BigQueryColumn][] = Object.entries(columns).filter(([_, col]) => + !col.shouldDisableInsert() + ); + + const insertOrder = colEntries.map( + ([, column]) => sql.identifier(this.casing.getColumnCasing(column)), + ); + + if (select) { + if (is(select, SQL)) { + valuesSqlList.push(select); + } else { + valuesSqlList.push((select as any).getSQL()); + } + } else { + valuesSqlList.push(sql.raw('values ')); + + for (const [valueIndex, value] of values.entries()) { + const valueList: (SQLChunk | SQL)[] = []; + for (const [fieldName, col] of colEntries) { + const colValue = value[fieldName]; + if (colValue === undefined || (is(colValue, Param) && colValue.value === undefined)) { + if (col.defaultFn !== undefined) { + const defaultFnResult = col.defaultFn(); + const defaultValue = is(defaultFnResult, SQL) ? defaultFnResult : sql.param(defaultFnResult, col); + valueList.push(defaultValue); + } 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 { + valueList.push(colValue); + } + } + + valuesSqlList.push(valueList); + if (valueIndex < values.length - 1) { + valuesSqlList.push(sql`, `); + } + } + } + + const withSql = this.buildWithCTE(withList); + + const valuesSql = sql.join(valuesSqlList); + + return sql`${withSql}insert into ${table} ${insertOrder} ${valuesSql}`; + } + + prepareTyping(encoder: DriverValueEncoder): QueryTypingsValue { + // BigQuery doesn't need special typing like PostgreSQL + return 'none'; + } + + sqlToQuery(sql: SQL, invokeSource?: 'indexes' | undefined): QueryWithTypings { + return sql.toQuery({ + casing: this.casing, + escapeName: this.escapeName, + escapeParam: this.escapeParam, + escapeString: this.escapeString, + prepareTyping: this.prepareTyping, + invokeSource, + }); + } + + buildRelationalQueryWithoutPK({ + fullSchema, + schema, + tableNamesMap, + table, + tableConfig, + queryConfig: config, + tableAlias, + nestedQueryRelation, + joinOn, + }: { + fullSchema: Record; + schema: TablesRelationalConfig; + tableNamesMap: Record; + table: BigQueryTable; + tableConfig: TableRelationalConfig; + queryConfig: true | DBQueryConfig<'many', true>; + tableAlias: string; + nestedQueryRelation?: Relation; + joinOn?: SQL; + }): BuildRelationalQueryResult { + let selection: BuildRelationalQueryResult['selection'] = []; + let limit, offset, orderBy: NonNullable = [], where; + const joins: BigQuerySelectJoinConfig[] = []; + + if (config === true) { + const selectionEntries = Object.entries(tableConfig.columns); + selection = selectionEntries.map(( + [key, value], + ) => ({ + dbKey: value.name, + tsKey: key, + field: aliasedTableColumn(value as BigQueryColumn, 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: BigQueryColumn | SQL.Aliased }[] = []; + let selectedColumns: string[] = []; + + 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 { + selectedColumns = Object.keys(tableConfig.columns); + } + + for (const field of selectedColumns) { + const column = tableConfig.columns[field]! as BigQueryColumn; + fieldsSelection.push({ tsKey: field, value: column }); + } + + let selectedRelations: { + tsKey: string; + queryConfig: true | DBQueryConfig<'many', false>; + relation: Relation; + }[] = []; + + 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; + + 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), + }); + } + } + + 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 BigQueryColumn; + } + return mapColumnsInSQLToAlias(orderByValue, tableAlias); + }); + + limit = config.limit; + offset = config.offset; + + 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.buildRelationalQueryWithoutPK({ + fullSchema, + schema, + tableNamesMap, + table: fullSchema[relationTableTsName] as BigQueryTable, + 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', + }); + 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) { + // BigQuery uses JSON functions differently - using TO_JSON_STRING and JSON_ARRAY + 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(array_agg(${field}${ + orderBy.length > 0 ? sql` order by ${sql.join(orderBy, sql`, `)}` : undefined + }), [])`; + } + 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; + + if (needsSubquery) { + result = this.buildSelectQuery({ + table: aliasedTable(table, tableAlias), + fields: {}, + fieldsFlat: [{ + path: [], + field: sql.raw('*'), + }], + where, + limit, + offset, + orderBy, + setOperators: [], + }); + + where = undefined; + limit = undefined; + offset = undefined; + orderBy = []; + } else { + result = aliasedTable(table, tableAlias); + } + + result = this.buildSelectQuery({ + table: is(result, BigQueryTable) ? 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, + }; + } +} diff --git a/drizzle-orm/src/bigquery-core/index.ts b/drizzle-orm/src/bigquery-core/index.ts new file mode 100644 index 0000000000..ae530274ec --- /dev/null +++ b/drizzle-orm/src/bigquery-core/index.ts @@ -0,0 +1,12 @@ +export * from './alias.ts'; +export * from './checks.ts'; +export * from './columns/index.ts'; +export * from './db.ts'; +export * from './dialect.ts'; +export * from './primary-keys.ts'; +export * from './query-builders/index.ts'; +export * from './session.ts'; +export * from './subquery.ts'; +export * from './table.ts'; +export * from './unique-constraint.ts'; +export * from './view-base.ts'; diff --git a/drizzle-orm/src/bigquery-core/primary-keys.ts b/drizzle-orm/src/bigquery-core/primary-keys.ts new file mode 100644 index 0000000000..06826a8a82 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/primary-keys.ts @@ -0,0 +1,63 @@ +import { entityKind } from '~/entity.ts'; +import type { AnyBigQueryColumn, BigQueryColumn } from './columns/index.ts'; +import { BigQueryTable } from './table.ts'; + +export function primaryKey< + TTableName extends string, + TColumn extends AnyBigQueryColumn<{ tableName: TTableName }>, + TColumns extends AnyBigQueryColumn<{ 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 AnyBigQueryColumn<{ 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 = 'BigQueryPrimaryKeyBuilder'; + + /** @internal */ + columns: BigQueryColumn[]; + + /** @internal */ + name?: string; + + constructor( + columns: BigQueryColumn[], + name?: string, + ) { + this.columns = columns; + this.name = name; + } + + /** @internal */ + build(table: BigQueryTable): PrimaryKey { + return new PrimaryKey(table, this.columns, this.name); + } +} + +export class PrimaryKey { + static readonly [entityKind]: string = 'BigQueryPrimaryKey'; + + readonly columns: BigQueryColumn[]; + readonly name?: string; + + constructor(readonly table: BigQueryTable, columns: BigQueryColumn[], name?: string) { + this.columns = columns; + this.name = name; + } + + getName(): string { + return this.name + ?? `${this.table[BigQueryTable.Symbol.Name]}_${this.columns.map((column) => column.name).join('_')}_pk`; + } +} diff --git a/drizzle-orm/src/bigquery-core/query-builders/count.ts b/drizzle-orm/src/bigquery-core/query-builders/count.ts new file mode 100644 index 0000000000..273e09b6e5 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/query-builders/count.ts @@ -0,0 +1,78 @@ +import { entityKind } from '~/entity.ts'; +import { SQL, sql, type SQLWrapper } from '~/sql/sql.ts'; +import type { BigQuerySession } from '../session.ts'; +import type { BigQueryTable } from '../table.ts'; + +export class BigQueryCountBuilder< + TSession extends BigQuerySession, +> extends SQL implements Promise, SQLWrapper { + private sql: SQL; + + static override readonly [entityKind] = 'BigQueryCountBuilder'; + [Symbol.toStringTag] = 'BigQueryCountBuilder'; + + private session: TSession; + + private static buildEmbeddedCount( + source: BigQueryTable | SQL | SQLWrapper, + filters?: SQL, + ): SQL { + return sql`(select count(*) from ${source}${sql.raw(' where ').if(filters)}${filters})`; + } + + private static buildCount( + source: BigQueryTable | SQL | SQLWrapper, + filters?: SQL, + ): SQL { + return sql`select count(*) as count from ${source}${sql.raw(' where ').if(filters)}${filters}`; + } + + constructor( + readonly params: { + source: BigQueryTable | SQL | SQLWrapper; + filters?: SQL; + session: TSession; + }, + ) { + super(BigQueryCountBuilder.buildEmbeddedCount(params.source, params.filters).queryChunks); + + this.mapWith(Number); + + this.session = params.session; + + this.sql = BigQueryCountBuilder.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) => any) | 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/bigquery-core/query-builders/delete.ts b/drizzle-orm/src/bigquery-core/query-builders/delete.ts new file mode 100644 index 0000000000..96fbc1cc89 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/query-builders/delete.ts @@ -0,0 +1,118 @@ +import type { BigQueryDialect } from '~/bigquery-core/dialect.ts'; +import type { + BigQueryPreparedQuery, + BigQueryQueryResultHKT, + BigQueryQueryResultKind, + BigQuerySession, + PreparedQueryConfig, +} from '~/bigquery-core/session.ts'; +import type { BigQueryTable } from '~/bigquery-core/table.ts'; +import { entityKind, is } from '~/entity.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts'; +import type { Subquery } from '~/subquery.ts'; +import { tracer } from '~/tracing.ts'; +import type { SelectedFieldsOrdered } from './select.types.ts'; + +export interface BigQueryDeleteConfig { + table: BigQueryTable; + where?: SQL; + withList?: Subquery[]; +} + +export type BigQueryDeleteWithout< + T extends AnyBigQueryDeleteBase, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true ? T + : Omit< + BigQueryDeleteBase< + T['_']['table'], + T['_']['queryResult'], + T['_']['dynamic'], + T['_']['excludedMethods'] | K + >, + T['_']['excludedMethods'] | K + >; + +export type BigQueryDeletePrepare = BigQueryPreparedQuery< + PreparedQueryConfig & { + execute: BigQueryQueryResultKind; + } +>; + +export type BigQueryDeleteDynamic = BigQueryDeleteBase< + T['_']['table'], + T['_']['queryResult'], + true, + never +>; + +export type AnyBigQueryDeleteBase = BigQueryDeleteBase; + +// BigQuery DELETE doesn't support RETURNING clause +export class BigQueryDeleteBase< + TTable extends BigQueryTable, + TQueryResult extends BigQueryQueryResultHKT, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, +> extends QueryPromise> { + static override readonly [entityKind]: string = 'BigQueryDelete'; + + declare readonly _: { + readonly table: TTable; + readonly queryResult: TQueryResult; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + }; + + private config: BigQueryDeleteConfig; + + constructor( + table: TTable, + private session: BigQuerySession, + private dialect: BigQueryDialect, + withList?: Subquery[], + ) { + super(); + this.config = { table, withList }; + } + + where( + where: SQL | undefined, + ): BigQueryDeleteWithout { + this.config.where = where; + 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(name: string): BigQueryDeletePrepare { + return tracer.startActiveSpan('drizzle.prepareQuery', () => { + return this.session.prepareQuery( + this.dialect.sqlToQuery(this.getSQL()), + undefined, + name, + false, + ) as BigQueryDeletePrepare; + }); + } + + override execute: ReturnType['execute'] = (placeholderValues) => { + return tracer.startActiveSpan('drizzle.operation', () => { + return this.prepare('execute').execute(placeholderValues); + }); + }; + + $dynamic(): BigQueryDeleteDynamic { + return this as any; + } +} diff --git a/drizzle-orm/src/bigquery-core/query-builders/index.ts b/drizzle-orm/src/bigquery-core/query-builders/index.ts new file mode 100644 index 0000000000..e6c69df996 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/query-builders/index.ts @@ -0,0 +1,12 @@ +export * from './count.ts'; +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'; + +// Re-export config types from their respective modules for dialect.ts +export type { BigQueryDeleteConfig } from './delete.ts'; +export type { BigQueryInsertConfig } from './insert.ts'; +export type { BigQueryUpdateConfig } from './update.ts'; diff --git a/drizzle-orm/src/bigquery-core/query-builders/insert.ts b/drizzle-orm/src/bigquery-core/query-builders/insert.ts new file mode 100644 index 0000000000..814a7b7e3a --- /dev/null +++ b/drizzle-orm/src/bigquery-core/query-builders/insert.ts @@ -0,0 +1,150 @@ +import type { BigQueryDialect } from '~/bigquery-core/dialect.ts'; +import type { + BigQueryPreparedQuery, + BigQueryQueryResultHKT, + BigQueryQueryResultKind, + BigQuerySession, + PreparedQueryConfig, +} from '~/bigquery-core/session.ts'; +import type { BigQueryTable } from '~/bigquery-core/table.ts'; +import { entityKind, is } from '~/entity.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { Placeholder, Query, SQLWrapper } from '~/sql/sql.ts'; +import { Param, SQL, sql } from '~/sql/sql.ts'; +import type { Subquery } from '~/subquery.ts'; +import { Table } from '~/table.ts'; +import type { InferModelFromColumns } from '~/table.ts'; +import { tracer } from '~/tracing.ts'; +import type { Simplify } from '~/utils.ts'; +import { mapUpdateSet, orderSelectedFields } from '~/utils.ts'; +import type { BigQueryColumn } from '../columns/common.ts'; +import type { SelectedFieldsOrdered } from './select.types.ts'; + +export interface BigQueryInsertConfig { + table: TTable; + values: Record[]; + withList?: Subquery[]; + select?: SQL; +} + +export type BigQueryInsertValue = Simplify< + { + [Key in keyof TTable['$inferInsert']]: TTable['$inferInsert'][Key] | SQL | Placeholder; + } +>; + +export class BigQueryInsertBuilder< + TTable extends BigQueryTable, + TQueryResult extends BigQueryQueryResultHKT, +> { + static readonly [entityKind]: string = 'BigQueryInsertBuilder'; + + constructor( + private table: TTable, + private session: BigQuerySession, + private dialect: BigQueryDialect, + private withList?: Subquery[], + ) {} + + values( + value: BigQueryInsertValue, + ): BigQueryInsertBase; + values( + values: BigQueryInsertValue[], + ): BigQueryInsertBase; + values( + values: BigQueryInsertValue | BigQueryInsertValue[], + ): BigQueryInsertBase { + 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 BigQueryInsertBase(this.table, mappedValues, this.session, this.dialect, this.withList); + } + + select( + selectQuery: SQL | ((qb: any) => SQL), + ): BigQueryInsertBase { + const select = typeof selectQuery === 'function' ? selectQuery(undefined) : selectQuery; + + return new BigQueryInsertBase(this.table, [], this.session, this.dialect, this.withList, select); + } +} + +export type BigQueryInsertPrepare< + TTable extends BigQueryTable, + TQueryResult extends BigQueryQueryResultHKT, +> = BigQueryPreparedQuery< + PreparedQueryConfig & { + execute: BigQueryQueryResultKind; + } +>; + +// BigQuery INSERT doesn't support RETURNING clause +export class BigQueryInsertBase< + TTable extends BigQueryTable, + TQueryResult extends BigQueryQueryResultHKT, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _TReturning extends Record | undefined = undefined, +> extends QueryPromise> { + static override readonly [entityKind]: string = 'BigQueryInsert'; + + declare _: { + readonly table: TTable; + }; + + private config: BigQueryInsertConfig; + + constructor( + table: TTable, + values: BigQueryInsertConfig['values'], + private session: BigQuerySession, + private dialect: BigQueryDialect, + withList?: Subquery[], + select?: SQL, + ) { + super(); + this.config = { table, values, withList, select }; + } + + /** @internal */ + getSQL(): SQL { + return this.dialect.buildInsertQuery(this.config); + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + prepare(name: string): BigQueryInsertPrepare { + return tracer.startActiveSpan('drizzle.prepareQuery', () => { + return this.session.prepareQuery( + this.dialect.sqlToQuery(this.getSQL()), + undefined, + name, + false, + ) as BigQueryInsertPrepare; + }); + } + + override execute: ReturnType['execute'] = (placeholderValues) => { + return tracer.startActiveSpan('drizzle.operation', () => { + return this.prepare('execute').execute(placeholderValues); + }); + }; + + $dynamic(): this { + return this; + } +} diff --git a/drizzle-orm/src/bigquery-core/query-builders/query-builder.ts b/drizzle-orm/src/bigquery-core/query-builders/query-builder.ts new file mode 100644 index 0000000000..52cb4af0d3 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/query-builders/query-builder.ts @@ -0,0 +1,112 @@ +import { BigQueryDialect } from '~/bigquery-core/dialect.ts'; +import { entityKind, is } from '~/entity.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 type { BigQueryColumn } from '../columns/index.ts'; +import type { WithBuilder } from '../subquery.ts'; +import { BigQuerySelectBuilder } from './select.ts'; +import type { SelectedFields } from './select.types.ts'; + +export class QueryBuilder { + static readonly [entityKind]: string = 'BigQueryQueryBuilder'; + + private dialect: BigQueryDialect | undefined; + + constructor(dialect?: BigQueryDialect) { + this.dialect = is(dialect, BigQueryDialect) ? dialect : undefined; + } + + $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(): BigQuerySelectBuilder; + function select(fields: TSelection): BigQuerySelectBuilder; + function select( + fields?: TSelection, + ): BigQuerySelectBuilder { + return new BigQuerySelectBuilder({ + fields: fields ?? undefined, + session: undefined, + dialect: self.getDialect(), + withList: queries, + }); + } + + function selectDistinct(): BigQuerySelectBuilder; + function selectDistinct( + fields: TSelection, + ): BigQuerySelectBuilder; + function selectDistinct( + fields?: TSelection, + ): BigQuerySelectBuilder { + return new BigQuerySelectBuilder({ + fields: fields ?? undefined, + session: undefined, + dialect: self.getDialect(), + withList: queries, + distinct: true, + }); + } + + return { select, selectDistinct }; + } + + select(): BigQuerySelectBuilder; + select(fields: TSelection): BigQuerySelectBuilder; + select(fields?: TSelection): BigQuerySelectBuilder { + return new BigQuerySelectBuilder({ + fields: fields ?? undefined, + session: undefined, + dialect: this.getDialect(), + }); + } + + selectDistinct(): BigQuerySelectBuilder; + selectDistinct(fields: TSelection): BigQuerySelectBuilder; + selectDistinct( + fields?: TSelection, + ): BigQuerySelectBuilder { + return new BigQuerySelectBuilder({ + 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 BigQueryDialect(); + } + + return this.dialect; + } +} diff --git a/drizzle-orm/src/bigquery-core/query-builders/select.ts b/drizzle-orm/src/bigquery-core/query-builders/select.ts new file mode 100644 index 0000000000..cdc00803cd --- /dev/null +++ b/drizzle-orm/src/bigquery-core/query-builders/select.ts @@ -0,0 +1,422 @@ +import type { BigQueryColumn } from '~/bigquery-core/columns/index.ts'; +import type { BigQueryDialect } from '~/bigquery-core/dialect.ts'; +import type { BigQuerySession, PreparedQueryConfig } from '~/bigquery-core/session.ts'; +import { BigQueryTable } from '~/bigquery-core/table.ts'; +import { BigQueryViewBase } from '~/bigquery-core/view-base.ts'; +import { entityKind, is } from '~/entity.ts'; +import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { + BuildSubquerySelection, + JoinNullability, + JoinType, + SelectMode, + SelectResult, +} 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 { tracer } from '~/tracing.ts'; +import { applyMixins, haveSameKeys, orderSelectedFields, type PromiseOf, type ValueOrArray } from '~/utils.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import type { + AnyBigQuerySelect, + AnyBigQuerySelectQueryBuilder, + BigQueryCreateSetOperatorFn, + BigQuerySelectConfig, + BigQuerySelectDynamic, + BigQuerySelectHKT, + BigQuerySelectHKTBase, + BigQuerySelectJoinConfig, + BigQuerySelectJoinFn, + BigQuerySelectKind, + BigQuerySelectPrepare, + BigQuerySelectQueryBuilderHKT, + BigQuerySelectWithout, + BigQuerySetOperatorExcludedMethods, + BigQuerySetOperatorWithResult, + CreateBigQuerySelectFromBuilderMode, + SelectedFields, + SelectedFieldsFlat, + SelectedFieldsOrdered, + SetOperatorRestSelect, + SetOperatorRightSelect, +} from './select.types.ts'; + +export class BigQuerySelectBuilder< + TSelection extends SelectedFields | undefined, + TBuilderMode extends 'db' | 'qb' = 'db', +> { + static readonly [entityKind]: string = 'BigQuerySelectBuilder'; + + private fields: TSelection; + private session: BigQuerySession | undefined; + private dialect: BigQueryDialect; + private withList: Subquery[] = []; + private distinct: boolean | undefined; + + constructor( + config: { + fields: TSelection; + session: BigQuerySession | undefined; + dialect: BigQueryDialect; + 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, + ): CreateBigQuerySelectFromBuilderMode< + TBuilderMode, + TFrom extends BigQueryTable ? TFrom['_']['name'] + : TFrom extends Subquery ? TFrom['_']['alias'] + : TFrom extends BigQueryViewBase ? TFrom['_']['name'] + : TFrom extends SQL ? undefined + : never, + TSelection extends undefined ? GetSelectTableSelection : TSelection, + TSelection extends undefined ? 'single' : 'partial' + > { + const isPartialSelect = !!this.fields; + + let fields: SelectedFields; + if (this.fields) { + fields = this.fields; + } else if (is(source, Subquery)) { + fields = Object.fromEntries( + Object.keys(source._.selectedFields).map((key) => [key, source[key as unknown as keyof typeof source]]), + ); + } else if (is(source, BigQueryViewBase)) { + fields = source[ViewBaseConfig].selectedFields as SelectedFields; + } else if (is(source, SQL)) { + fields = {}; + } else { + fields = source[Table.Symbol.Columns]; + } + + return new BigQuerySelectBase({ + table: source, + fields, + isPartialSelect, + session: this.session, + dialect: this.dialect, + withList: this.withList, + distinct: this.distinct, + }) as any; + } +} + +export type GetSelectTableSelection = T extends BigQueryTable ? T['_']['columns'] + : T extends Subquery | View ? T['_']['selectedFields'] + : T extends SQL ? {} + : never; + +export abstract class BigQuerySelectQueryBuilderBase< + THKT extends BigQuerySelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + 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 = 'BigQuerySelectQueryBuilder'; + + override readonly _: { + readonly hkt: THKT; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + + protected config: BigQuerySelectConfig; + protected joinsNotNullableMap: Record; + private tableName: string | undefined; + protected session: BigQuerySession | undefined; + protected dialect: BigQueryDialect; + + constructor( + { table, fields, isPartialSelect, session, dialect, withList, distinct }: { + table: BigQuerySelectConfig['table']; + fields: BigQuerySelectConfig['fields']; + isPartialSelect: boolean; + session: BigQuerySession | undefined; + dialect: BigQueryDialect; + withList: Subquery[] | undefined; + distinct: boolean | undefined; + }, + ) { + super(); + this.config = { + withList, + table, + fields: { ...fields }, + distinct, + setOperators: [], + }; + this.joinsNotNullableMap = typeof this.tableName === 'string' ? { [this.tableName]: true } : {}; + this.tableName = getTableLikeName(table); + this.session = session; + this.dialect = dialect; + this._ = { + selectedFields: fields as TSelectedFields, + } as this['_']; + } + + private createJoin( + joinType: TJoinType, + ): BigQuerySelectJoinFn { + return (( + table: BigQueryTable | Subquery | BigQueryViewBase | SQL, + on: ((aliases: TSelection) => SQL | undefined) | SQL | undefined, + ) => { + 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.joinsNotNullableMap[baseTableName!]) { + this.joinsNotNullableMap[baseTableName!] = true; + } + + 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 = []; + } + this.config.joins.push({ on, table, joinType, alias: tableName }); + + 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; + }) as BigQuerySelectJoinFn; + } + + leftJoin = this.createJoin('left'); + rightJoin = this.createJoin('right'); + innerJoin = this.createJoin('inner'); + fullJoin = this.createJoin('full'); + crossJoin = this.createJoin('cross'); + + where( + where: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, + ): BigQuerySelectWithout { + 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; + } + + having( + having: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, + ): BigQuerySelectWithout { + 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; + } + + groupBy( + ...columns: (BigQueryColumn | SQL)[] + ): BigQuerySelectWithout { + this.config.groupBy = columns as (BigQueryColumn | SQL | SQL.Aliased)[]; + return this as any; + } + + orderBy( + ...columns: (BigQueryColumn | SQL)[] + ): BigQuerySelectWithout { + this.config.orderBy = columns as (BigQueryColumn | SQL | SQL.Aliased)[]; + return this as any; + } + + limit(limit: number | Placeholder): BigQuerySelectWithout { + this.config.limit = limit; + return this as any; + } + + offset(offset: number | Placeholder): BigQuerySelectWithout { + this.config.offset = offset; + return this as any; + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + override getSQL(): SQL { + return this.dialect.buildSelectQuery(this.config); + } + + as( + alias: TAlias, + ): SubqueryWithSelection { + return new Proxy( + new Subquery(this.getSQL(), this.config.fields, alias), + new SelectionProxyHandler({ + alias, + sqlAliasedBehavior: 'alias', + sqlBehavior: 'error', + }), + ) as SubqueryWithSelection; + } + + $dynamic(): BigQuerySelectDynamic { + return this as any; + } +} + +export interface BigQuerySelectBase< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends + BigQuerySelectQueryBuilderBase< + BigQuerySelectHKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + > +{} + +export class BigQuerySelectBase< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends BigQuerySelectQueryBuilderBase< + BigQuerySelectHKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields +> { + static override readonly [entityKind]: string = 'BigQuerySelect'; + + prepare(name: string): BigQuerySelectPrepare { + const { session, dialect, config, joinsNotNullableMap } = this; + if (!session) { + throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); + } + return tracer.startActiveSpan('drizzle.prepareQuery', () => { + const fieldsList = orderSelectedFields(config.fields); + const query = session.prepareQuery< + PreparedQueryConfig & { execute: TResult } + >(dialect.sqlToQuery(this.getSQL()), fieldsList, name, true); + query.joinsNotNullableMap = joinsNotNullableMap; + return query as BigQuerySelectPrepare; + }); + } + + execute: ReturnType['execute'] = (placeholderValues) => { + return tracer.startActiveSpan('drizzle.operation', () => { + return this.prepare('execute').execute(placeholderValues); + }); + }; +} + +applyMixins(BigQuerySelectBase, [QueryPromise]); + +export type SubqueryWithSelection = + & Subquery< + TAlias, + TSelection + > + & TSelection; + +function getTableLikeName(table: BigQueryTable | Subquery | BigQueryViewBase | SQL): string | undefined { + return is(table, Subquery) + ? table._.alias + : is(table, BigQueryViewBase) + ? table[ViewBaseConfig].name + : is(table, SQL) + ? undefined + : table[Table.Symbol.IsAlias] + ? table[Table.Symbol.Name] + : table[Table.Symbol.BaseName]; +} diff --git a/drizzle-orm/src/bigquery-core/query-builders/select.types.ts b/drizzle-orm/src/bigquery-core/query-builders/select.types.ts new file mode 100644 index 0000000000..57e5566974 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/query-builders/select.types.ts @@ -0,0 +1,405 @@ +import type { BigQueryColumn } from '~/bigquery-core/columns/index.ts'; +import type { BigQueryTable, BigQueryTableWithColumns } from '~/bigquery-core/table.ts'; +import type { BigQueryViewBase } from '~/bigquery-core/view-base.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, SQLWrapper, View } from '~/sql/sql.ts'; +import type { Subquery } from '~/subquery.ts'; +import type { Table, UpdateTableConfig } from '~/table.ts'; +import type { Assume, DrizzleTypeError, Equal, ValidateShape, ValueOrArray } from '~/utils.ts'; +import type { BigQueryPreparedQuery, PreparedQueryConfig } from '../session.ts'; +import type { BigQuerySelectBase, BigQuerySelectQueryBuilderBase } from './select.ts'; + +export interface BigQuerySelectJoinConfig { + on: SQL | undefined; + table: BigQueryTable | Subquery | BigQueryViewBase | SQL; + alias: string | undefined; + joinType: JoinType; + // Note: BigQuery doesn't support LATERAL joins like PostgreSQL +} + +export type BuildAliasTable = TTable extends Table + ? BigQueryTableWithColumns< + UpdateTableConfig; + }> + > + : never; + +export interface BigQuerySelectConfig { + withList?: Subquery[]; + fields: Record; + fieldsFlat?: SelectedFieldsOrdered; + where?: SQL; + having?: SQL; + table: BigQueryTable | Subquery | BigQueryViewBase | SQL; + limit?: number | Placeholder; + offset?: number | Placeholder; + joins?: BigQuerySelectJoinConfig[]; + orderBy?: (BigQueryColumn | SQL | SQL.Aliased)[]; + groupBy?: (BigQueryColumn | SQL | SQL.Aliased)[]; + // BigQuery only supports DISTINCT (not DISTINCT ON) + distinct?: boolean; + setOperators: { + rightSelect: TypedQueryBuilder; + type: SetOperator; + isAll: boolean; + orderBy?: (BigQueryColumn | SQL | SQL.Aliased)[]; + limit?: number | Placeholder; + offset?: number | Placeholder; + }[]; +} + +export type TableLikeHasEmptySelection = T extends Subquery + ? Equal extends true ? true : false + : false; + +export type BigQuerySelectJoin< + T extends AnyBigQuerySelectQueryBuilder, + TDynamic extends boolean, + TJoinType extends JoinType, + TJoinedTable extends BigQueryTable | Subquery | BigQueryViewBase | SQL, + TJoinedName extends GetSelectTableName = GetSelectTableName, +> = T extends any ? BigQuerySelectWithout< + BigQuerySelectKind< + T['_']['hkt'], + T['_']['tableName'], + AppendToResult< + T['_']['tableName'], + T['_']['selection'], + TJoinedName, + TJoinedTable extends Table ? TJoinedTable['_']['columns'] + : TJoinedTable extends Subquery | View ? Assume + : never, + T['_']['selectMode'] + >, + T['_']['selectMode'] extends 'partial' ? T['_']['selectMode'] : 'multiple', + AppendToNullabilityMap, + T['_']['dynamic'], + T['_']['excludedMethods'] + >, + TDynamic, + T['_']['excludedMethods'] + > + : never; + +export type BigQuerySelectJoinFn< + T extends AnyBigQuerySelectQueryBuilder, + TDynamic extends boolean, + TJoinType extends JoinType, +> = < + TJoinedTable extends BigQueryTable | Subquery | BigQueryViewBase | SQL, + TJoinedName extends GetSelectTableName = GetSelectTableName, +>( + table: TableLikeHasEmptySelection extends true ? DrizzleTypeError< + "Cannot reference a data-modifying statement subquery if it doesn't contain a `returning` clause" + > + : TJoinedTable, + on: ((aliases: T['_']['selection']) => SQL | undefined) | SQL | undefined, +) => BigQuerySelectJoin; + +export type BigQuerySelectCrossJoinFn< + T extends AnyBigQuerySelectQueryBuilder, + TDynamic extends boolean, +> = < + TJoinedTable extends BigQueryTable | Subquery | BigQueryViewBase | SQL, + TJoinedName extends GetSelectTableName = GetSelectTableName, +>( + table: TableLikeHasEmptySelection extends true ? DrizzleTypeError< + "Cannot reference a data-modifying statement subquery if it doesn't contain a `returning` clause" + > + : TJoinedTable, +) => BigQuerySelectJoin; + +export type SelectedFieldsFlat = SelectedFieldsFlatBase; + +export type SelectedFields = SelectedFieldsBase; + +export type SelectedFieldsOrdered = SelectedFieldsOrderedBase; + +export interface BigQuerySelectHKTBase { + tableName: string | undefined; + selection: unknown; + selectMode: SelectMode; + nullabilityMap: unknown; + dynamic: boolean; + excludedMethods: string; + result: unknown; + selectedFields: unknown; + _type: unknown; +} + +export type BigQuerySelectKind< + T extends BigQuerySelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TDynamic extends boolean, + TExcludedMethods extends string, + TResult = SelectResult[], + TSelectedFields = BuildSubquerySelection, +> = (T & { + tableName: TTableName; + selection: TSelection; + selectMode: TSelectMode; + nullabilityMap: TNullabilityMap; + dynamic: TDynamic; + excludedMethods: TExcludedMethods; + result: TResult; + selectedFields: TSelectedFields; +})['_type']; + +export interface BigQuerySelectQueryBuilderHKT extends BigQuerySelectHKTBase { + _type: BigQuerySelectQueryBuilderBase< + BigQuerySelectQueryBuilderHKT, + this['tableName'], + Assume, + this['selectMode'], + Assume>, + this['dynamic'], + this['excludedMethods'], + Assume, + Assume + >; +} + +export interface BigQuerySelectHKT extends BigQuerySelectHKTBase { + _type: BigQuerySelectBase< + this['tableName'], + Assume, + this['selectMode'], + Assume>, + this['dynamic'], + this['excludedMethods'], + Assume, + Assume + >; +} + +export type CreateBigQuerySelectFromBuilderMode< + TBuilderMode extends 'db' | 'qb', + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, +> = TBuilderMode extends 'db' ? BigQuerySelectBase + : BigQuerySelectQueryBuilderBase; + +export type BigQuerySetOperatorExcludedMethods = + | 'leftJoin' + | 'rightJoin' + | 'innerJoin' + | 'fullJoin' + | 'where' + | 'having' + | 'groupBy'; + +export type BigQuerySelectWithout< + T extends AnyBigQuerySelectQueryBuilder, + TDynamic extends boolean, + K extends keyof T & string, + TResetExcluded extends boolean = false, +> = TDynamic extends true ? T : Omit< + BigQuerySelectKind< + T['_']['hkt'], + T['_']['tableName'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + TDynamic, + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K, + T['_']['result'], + T['_']['selectedFields'] + >, + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K +>; + +export type BigQuerySelectPrepare = BigQueryPreparedQuery< + PreparedQueryConfig & { + execute: T['_']['result']; + } +>; + +export type BigQuerySelectDynamic = BigQuerySelectKind< + T['_']['hkt'], + T['_']['tableName'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + true, + never, + T['_']['result'], + T['_']['selectedFields'] +>; + +export type BigQuerySelectQueryBuilder< + THKT extends BigQuerySelectHKTBase = BigQuerySelectQueryBuilderHKT, + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = ColumnsSelection, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, + TResult extends any[] = unknown[], + TSelectedFields extends ColumnsSelection = ColumnsSelection, +> = BigQuerySelectQueryBuilderBase< + THKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + true, + never, + TResult, + TSelectedFields +>; + +export type AnyBigQuerySelectQueryBuilder = BigQuerySelectQueryBuilderBase; + +export type AnyBigQuerySetOperatorInterface = BigQuerySetOperatorInterface; + +export interface BigQuerySetOperatorInterface< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + 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: BigQuerySelectHKT; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; +} + +export type BigQuerySetOperatorWithResult = BigQuerySetOperatorInterface< + any, + any, + any, + any, + any, + any, + TResult, + any +>; + +export type BigQuerySelect< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = BigQuerySelectBase; + +export type AnyBigQuerySelect = BigQuerySelectBase; + +export type BigQuerySetOperator< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = BigQuerySelectBase< + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + true, + BigQuerySetOperatorExcludedMethods +>; + +export type SetOperatorRightSelect< + TValue extends BigQuerySetOperatorWithResult, + TResult extends any[], +> = TValue extends BigQuerySetOperatorInterface ? ValidateShape< + TValueResult[number], + TResult[number], + TypedQueryBuilder + > + : TValue; + +export type SetOperatorRestSelect< + TValue extends readonly BigQuerySetOperatorWithResult[], + TResult extends any[], +> = TValue extends [infer First, ...infer Rest] + ? First extends BigQuerySetOperatorInterface + ? Rest extends AnyBigQuerySetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> + : never + : TValue; + +export type BigQueryCreateSetOperatorFn = < + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TValue extends BigQuerySetOperatorWithResult, + TRest extends BigQuerySetOperatorWithResult[], + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +>( + leftSelect: BigQuerySetOperatorInterface< + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect +) => BigQuerySelectWithout< + BigQuerySelectBase< + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + false, + BigQuerySetOperatorExcludedMethods, + true +>; + +export type GetBigQuerySetOperators = { + union: BigQueryCreateSetOperatorFn; + intersect: BigQueryCreateSetOperatorFn; + except: BigQueryCreateSetOperatorFn; + unionAll: BigQueryCreateSetOperatorFn; + intersectAll: BigQueryCreateSetOperatorFn; + exceptAll: BigQueryCreateSetOperatorFn; +}; diff --git a/drizzle-orm/src/bigquery-core/query-builders/update.ts b/drizzle-orm/src/bigquery-core/query-builders/update.ts new file mode 100644 index 0000000000..184c8bea15 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/query-builders/update.ts @@ -0,0 +1,163 @@ +import type { BigQueryDialect } from '~/bigquery-core/dialect.ts'; +import type { + BigQueryPreparedQuery, + BigQueryQueryResultHKT, + BigQueryQueryResultKind, + BigQuerySession, + PreparedQueryConfig, +} from '~/bigquery-core/session.ts'; +import type { BigQueryTable } from '~/bigquery-core/table.ts'; +import { entityKind, is } from '~/entity.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts'; +import { Param, sql } from '~/sql/sql.ts'; +import type { Subquery } from '~/subquery.ts'; +import { Table } from '~/table.ts'; +import { tracer } from '~/tracing.ts'; +import type { Simplify, UpdateSet } from '~/utils.ts'; +import { mapUpdateSet } from '~/utils.ts'; +import type { BigQueryColumn } from '../columns/common.ts'; +import type { SelectedFieldsOrdered } from './select.types.ts'; + +export interface BigQueryUpdateConfig { + table: BigQueryTable; + set: UpdateSet; + where?: SQL; + withList?: Subquery[]; +} + +export type BigQueryUpdateSetSource = Simplify< + { + [Key in keyof TTable['$inferInsert']]?: + | TTable['$inferInsert'][Key] + | SQL; + } +>; + +export class BigQueryUpdateBuilder< + TTable extends BigQueryTable, + TQueryResult extends BigQueryQueryResultHKT, +> { + static readonly [entityKind]: string = 'BigQueryUpdateBuilder'; + + declare readonly _: { + readonly table: TTable; + }; + + constructor( + private table: TTable, + private session: BigQuerySession, + private dialect: BigQueryDialect, + private withList?: Subquery[], + ) {} + + set( + values: BigQueryUpdateSetSource, + ): BigQueryUpdateBase { + return new BigQueryUpdateBase( + this.table, + mapUpdateSet(this.table, values), + this.session, + this.dialect, + this.withList, + ); + } +} + +export type BigQueryUpdateWithout< + T extends AnyBigQueryUpdateBase, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true ? T + : Omit< + BigQueryUpdateBase< + T['_']['table'], + T['_']['queryResult'], + T['_']['dynamic'], + T['_']['excludedMethods'] | K + >, + T['_']['excludedMethods'] | K + >; + +export type BigQueryUpdatePrepare = BigQueryPreparedQuery< + PreparedQueryConfig & { + execute: BigQueryQueryResultKind; + } +>; + +export type BigQueryUpdateDynamic = BigQueryUpdateBase< + T['_']['table'], + T['_']['queryResult'], + true, + never +>; + +export type AnyBigQueryUpdateBase = BigQueryUpdateBase; + +// BigQuery UPDATE doesn't support RETURNING clause +export class BigQueryUpdateBase< + TTable extends BigQueryTable, + TQueryResult extends BigQueryQueryResultHKT, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, +> extends QueryPromise> { + static override readonly [entityKind]: string = 'BigQueryUpdate'; + + declare readonly _: { + readonly table: TTable; + readonly queryResult: TQueryResult; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + }; + + private config: BigQueryUpdateConfig; + + constructor( + table: TTable, + set: UpdateSet, + private session: BigQuerySession, + private dialect: BigQueryDialect, + withList?: Subquery[], + ) { + super(); + this.config = { table, set, withList }; + } + + where( + where: SQL | undefined, + ): BigQueryUpdateWithout { + this.config.where = where; + 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(name: string): BigQueryUpdatePrepare { + return tracer.startActiveSpan('drizzle.prepareQuery', () => { + return this.session.prepareQuery( + this.dialect.sqlToQuery(this.getSQL()), + undefined, + name, + false, + ) as BigQueryUpdatePrepare; + }); + } + + override execute: ReturnType['execute'] = (placeholderValues) => { + return tracer.startActiveSpan('drizzle.operation', () => { + return this.prepare('execute').execute(placeholderValues); + }); + }; + + $dynamic(): BigQueryUpdateDynamic { + return this as any; + } +} diff --git a/drizzle-orm/src/bigquery-core/session.ts b/drizzle-orm/src/bigquery-core/session.ts new file mode 100644 index 0000000000..1ecfafeb6e --- /dev/null +++ b/drizzle-orm/src/bigquery-core/session.ts @@ -0,0 +1,144 @@ +import { entityKind } from '~/entity.ts'; +import { DrizzleQueryError, TransactionRollbackError } from '~/errors.ts'; +import type { TablesRelationalConfig } from '~/relations.ts'; +import type { PreparedQuery } from '~/session.ts'; +import { type Query, type SQL, sql } from '~/sql/index.ts'; +import { tracer } from '~/tracing.ts'; +import { BigQueryDatabase } from './db.ts'; +import type { BigQueryDialect } from './dialect.ts'; +import type { SelectedFieldsOrdered } from './query-builders/select.types.ts'; + +export interface PreparedQueryConfig { + execute: unknown; + all: unknown; + values: unknown; +} + +export abstract class BigQueryPreparedQuery implements PreparedQuery { + constructor( + protected query: Query, + ) {} + + getQuery(): Query { + return this.query; + } + + mapResult(response: unknown, _isFromBatch?: boolean): unknown { + return response; + } + + static readonly [entityKind]: string = 'BigQueryPreparedQuery'; + + /** @internal */ + joinsNotNullableMap?: Record; + + abstract execute(placeholderValues?: Record): Promise; + + /** @internal */ + abstract all(placeholderValues?: Record): Promise; + + /** @internal */ + abstract isResponseInArrayMode(): boolean; +} + +// BigQuery has limited transaction support via scripting +export interface BigQueryTransactionConfig { + // BigQuery doesn't support traditional isolation levels + // but we keep this for API compatibility +} + +export abstract class BigQuerySession< + TQueryResult extends BigQueryQueryResultHKT = BigQueryQueryResultHKT, + TFullSchema extends Record = Record, + TSchema extends TablesRelationalConfig = Record, +> { + static readonly [entityKind]: string = 'BigQuerySession'; + + constructor(protected dialect: BigQueryDialect) {} + + abstract prepareQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + name: string | undefined, + isResponseInArrayMode: boolean, + customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => T['execute'], + ): BigQueryPreparedQuery; + + execute(query: SQL): Promise { + return tracer.startActiveSpan('drizzle.operation', () => { + const prepared = tracer.startActiveSpan('drizzle.prepareQuery', () => { + return this.prepareQuery( + this.dialect.sqlToQuery(query), + undefined, + undefined, + false, + ); + }); + + return prepared.execute(); + }); + } + + all(query: SQL): Promise { + return this.prepareQuery( + this.dialect.sqlToQuery(query), + undefined, + undefined, + false, + ).all(); + } + + async count(sql: SQL): Promise { + const res = await this.execute<[{ count: string }]>(sql); + + return Number( + res[0]['count'], + ); + } + + // BigQuery has limited transaction support via multi-statement transactions + abstract transaction( + transaction: (tx: BigQueryTransaction) => Promise, + config?: BigQueryTransactionConfig, + ): Promise; +} + +export abstract class BigQueryTransaction< + TQueryResult extends BigQueryQueryResultHKT, + TFullSchema extends Record = Record, + TSchema extends TablesRelationalConfig = Record, +> extends BigQueryDatabase { + static override readonly [entityKind]: string = 'BigQueryTransaction'; + + constructor( + dialect: BigQueryDialect, + session: BigQuerySession, + protected schema: { + fullSchema: Record; + schema: TSchema; + tableNamesMap: Record; + } | undefined, + protected readonly nestedIndex = 0, + ) { + super(dialect, session, schema); + } + + rollback(): never { + throw new TransactionRollbackError(); + } + + // BigQuery transactions don't support savepoints or nested transactions in the traditional sense + abstract override transaction( + transaction: (tx: BigQueryTransaction) => Promise, + ): Promise; +} + +export interface BigQueryQueryResultHKT { + readonly $brand: 'BigQueryQueryResultHKT'; + readonly row: unknown; + readonly type: unknown; +} + +export type BigQueryQueryResultKind = (TKind & { + readonly row: TRow; +})['type']; diff --git a/drizzle-orm/src/bigquery-core/subquery.ts b/drizzle-orm/src/bigquery-core/subquery.ts new file mode 100644 index 0000000000..7291ebe024 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/subquery.ts @@ -0,0 +1,25 @@ +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 { WithSubquery, WithSubqueryWithoutSelection } from '~/subquery.ts'; +import type { QueryBuilder } from './query-builders/query-builder.ts'; + +export type WithSubqueryWithSelection = + & 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/bigquery-core/table.ts b/drizzle-orm/src/bigquery-core/table.ts new file mode 100644 index 0000000000..c292bf866d --- /dev/null +++ b/drizzle-orm/src/bigquery-core/table.ts @@ -0,0 +1,176 @@ +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 { type BigQueryColumnBuilders, getBigQueryColumnBuilders } from './columns/all.ts'; +import type { BigQueryColumn, BigQueryColumnBuilder, BigQueryColumnBuilderBase } from './columns/common.ts'; +import type { PrimaryKeyBuilder } from './primary-keys.ts'; +import type { UniqueConstraintBuilder } from './unique-constraint.ts'; + +export type BigQueryTableExtraConfigValue = + | CheckBuilder + | PrimaryKeyBuilder + | UniqueConstraintBuilder; + +export type BigQueryTableExtraConfig = Record< + string, + BigQueryTableExtraConfigValue +>; + +export type TableConfig = TableConfigBase; + +export class BigQueryTable extends Table { + static override readonly [entityKind]: string = 'BigQueryTable'; + + declare protected $columns: T['columns']; + + /** @internal */ + override [Table.Symbol.Columns]!: NonNullable; + + /** @internal */ + override [Table.Symbol.ExtraConfigBuilder]: + | ((self: Record) => BigQueryTableExtraConfig) + | undefined = undefined; +} + +export type AnyBigQueryTable = {}> = BigQueryTable< + UpdateTableConfig +>; + +export type BigQueryTableWithColumns = + & BigQueryTable + & { + [Key in keyof T['columns']]: T['columns'][Key]; + }; + +export function bigqueryTableWithDataset< + TTableName extends string, + TDatasetName extends string | undefined, + TColumnsMap extends Record, +>( + name: TTableName, + columns: TColumnsMap | ((columnTypes: BigQueryColumnBuilders) => TColumnsMap), + extraConfig: + | (( + self: BuildColumns, + ) => BigQueryTableExtraConfig | BigQueryTableExtraConfigValue[]) + | undefined, + dataset: TDatasetName, + baseName = name, +): BigQueryTableWithColumns<{ + name: TTableName; + schema: TDatasetName; + columns: BuildColumns; + dialect: 'bigquery'; +}> { + const rawTable = new BigQueryTable<{ + name: TTableName; + schema: TDatasetName; + columns: BuildColumns; + dialect: 'bigquery'; + }>(name, dataset, baseName); + + const parsedColumns: TColumnsMap = typeof columns === 'function' ? columns(getBigQueryColumnBuilders()) : columns; + + const builtColumns = Object.fromEntries( + Object.entries(parsedColumns).map(([name, colBuilderBase]) => { + const colBuilder = colBuilderBase as BigQueryColumnBuilder; + colBuilder.setName(name); + const column = colBuilder.build(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, + 'bigquery' + >; + + if (extraConfig) { + table[BigQueryTable.Symbol.ExtraConfigBuilder] = extraConfig as unknown as ( + self: Record, + ) => BigQueryTableExtraConfig; + } + + return table; +} + +export interface BigQueryTableFn { + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: TColumnsMap, + extraConfig?: ( + self: BuildColumns, + ) => BigQueryTableExtraConfigValue[], + ): BigQueryTableWithColumns<{ + name: TTableName; + schema: TDatasetName; + columns: BuildColumns; + dialect: 'bigquery'; + }>; + + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: (columnTypes: BigQueryColumnBuilders) => TColumnsMap, + extraConfig?: (self: BuildColumns) => BigQueryTableExtraConfigValue[], + ): BigQueryTableWithColumns<{ + name: TTableName; + schema: TDatasetName; + columns: BuildColumns; + dialect: 'bigquery'; + }>; + + /** + * @deprecated The third parameter of bigqueryTable is changing and will only accept an array instead of an object + */ + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: TColumnsMap, + extraConfig: (self: BuildColumns) => BigQueryTableExtraConfig, + ): BigQueryTableWithColumns<{ + name: TTableName; + schema: TDatasetName; + columns: BuildColumns; + dialect: 'bigquery'; + }>; + + /** + * @deprecated The third parameter of bigqueryTable is changing and will only accept an array instead of an object + */ + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: (columnTypes: BigQueryColumnBuilders) => TColumnsMap, + extraConfig: (self: BuildColumns) => BigQueryTableExtraConfig, + ): BigQueryTableWithColumns<{ + name: TTableName; + schema: TDatasetName; + columns: BuildColumns; + dialect: 'bigquery'; + }>; +} + +export const bigqueryTable: BigQueryTableFn = (name, columns, extraConfig) => { + return bigqueryTableWithDataset(name, columns, extraConfig, undefined, name); +}; + +export function bigqueryTableCreator(customizeTableName: (name: string) => string): BigQueryTableFn { + return (name, columns, extraConfig) => { + return bigqueryTableWithDataset(customizeTableName(name) as typeof name, columns, extraConfig, undefined, name); + }; +} diff --git a/drizzle-orm/src/bigquery-core/unique-constraint.ts b/drizzle-orm/src/bigquery-core/unique-constraint.ts new file mode 100644 index 0000000000..f3d95da528 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/unique-constraint.ts @@ -0,0 +1,65 @@ +import { entityKind } from '~/entity.ts'; +import { TableName } from '~/table.utils.ts'; +import type { BigQueryColumn } from './columns/index.ts'; +import type { BigQueryTable } from './table.ts'; + +export function unique(name?: string): UniqueOnConstraintBuilder { + return new UniqueOnConstraintBuilder(name); +} + +export function uniqueKeyName(table: BigQueryTable, columns: string[]) { + return `${table[TableName]}_${columns.join('_')}_unique`; +} + +export class UniqueConstraintBuilder { + static readonly [entityKind]: string = 'BigQueryUniqueConstraintBuilder'; + + /** @internal */ + columns: BigQueryColumn[]; + + constructor( + columns: BigQueryColumn[], + private name?: string, + ) { + this.columns = columns; + } + + /** @internal */ + build(table: BigQueryTable): UniqueConstraint { + return new UniqueConstraint(table, this.columns, this.name); + } +} + +export class UniqueOnConstraintBuilder { + static readonly [entityKind]: string = 'BigQueryUniqueOnConstraintBuilder'; + + /** @internal */ + name?: string; + + constructor( + name?: string, + ) { + this.name = name; + } + + on(...columns: [BigQueryColumn, ...BigQueryColumn[]]) { + return new UniqueConstraintBuilder(columns, this.name); + } +} + +export class UniqueConstraint { + static readonly [entityKind]: string = 'BigQueryUniqueConstraint'; + + readonly columns: BigQueryColumn[]; + readonly name?: string; + readonly nullsNotDistinct: boolean = false; + + constructor(readonly table: BigQueryTable, columns: BigQueryColumn[], 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/bigquery-core/view-base.ts b/drizzle-orm/src/bigquery-core/view-base.ts new file mode 100644 index 0000000000..d999aa9519 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/view-base.ts @@ -0,0 +1,14 @@ +import { entityKind } from '~/entity.ts'; +import { type ColumnsSelection, View } from '~/sql/sql.ts'; + +export abstract class BigQueryViewBase< + TName extends string = string, + TExisting extends boolean = boolean, + TSelectedFields extends ColumnsSelection = ColumnsSelection, +> extends View { + static override readonly [entityKind]: string = 'BigQueryViewBase'; + + declare readonly _: View['_'] & { + readonly viewBrand: 'BigQueryViewBase'; + }; +} diff --git a/drizzle-orm/src/bigquery/driver.ts b/drizzle-orm/src/bigquery/driver.ts new file mode 100644 index 0000000000..cf0f6f7963 --- /dev/null +++ b/drizzle-orm/src/bigquery/driver.ts @@ -0,0 +1,130 @@ +import type { BigQuery, BigQueryOptions } from '@google-cloud/bigquery'; +import { BigQueryDatabase } from '~/bigquery-core/db.ts'; +import { BigQueryDialect } from '~/bigquery-core/dialect.ts'; +import { entityKind } from '~/entity.ts'; +import type { Logger } from '~/logger.ts'; +import { DefaultLogger } from '~/logger.ts'; +import { + createTableRelationsHelpers, + extractTablesRelationalConfig, + type RelationalSchemaConfig, + type TablesRelationalConfig, +} from '~/relations.ts'; +import { type DrizzleConfig, isConfig } from '~/utils.ts'; +import type { BigQueryQueryResultHKT } from './session.ts'; +import { BigQueryClientSession } from './session.ts'; + +export interface BigQueryDriverOptions { + logger?: Logger; +} + +export class BigQueryDriver { + static readonly [entityKind]: string = 'BigQueryDriver'; + + constructor( + private client: BigQuery, + private dialect: BigQueryDialect, + private options: BigQueryDriverOptions = {}, + ) {} + + createSession( + schema: RelationalSchemaConfig | undefined, + ): BigQueryClientSession, TablesRelationalConfig> { + return new BigQueryClientSession(this.client, this.dialect, schema, { + logger: this.options.logger, + }); + } +} + +export class NodeBigQueryDatabase< + TSchema extends Record = Record, +> extends BigQueryDatabase { + static override readonly [entityKind]: string = 'NodeBigQueryDatabase'; +} + +function construct< + TSchema extends Record = Record, +>( + client: BigQuery, + config: DrizzleConfig = {}, +): NodeBigQueryDatabase & { + $client: BigQuery; +} { + const dialect = new BigQueryDialect({ casing: config.casing }); + let logger; + if (config.logger === true) { + logger = new DefaultLogger(); + } else if (config.logger !== false) { + logger = config.logger; + } + + let schema: RelationalSchemaConfig | undefined; + if (config.schema) { + const tablesConfig = extractTablesRelationalConfig( + config.schema, + createTableRelationsHelpers, + ); + schema = { + fullSchema: config.schema, + schema: tablesConfig.tables, + tableNamesMap: tablesConfig.tableNamesMap, + }; + } + + const driver = new BigQueryDriver(client, dialect, { logger }); + const session = driver.createSession(schema); + const db = new NodeBigQueryDatabase(dialect, session, schema as any) as NodeBigQueryDatabase; + ( db).$client = client; + + return db as any; +} + +/** + * Create a Drizzle ORM database instance for BigQuery. + * + * @param client - BigQuery client instance or configuration + * @param config - Optional Drizzle configuration + * + * @example + * ```ts + * import { BigQuery } from '@google-cloud/bigquery'; + * import { drizzle } from 'drizzle-orm/bigquery'; + * + * const bigquery = new BigQuery(); + * const db = drizzle(bigquery); + * + * // Or with configuration + * const db = drizzle(bigquery, { schema }); + * ``` + */ +export function drizzle< + TSchema extends Record = Record, +>( + ...params: + | [BigQuery] + | [BigQuery, DrizzleConfig] + | [DrizzleConfig & { client: BigQuery }] +): NodeBigQueryDatabase & { + $client: BigQuery; +} { + if (isConfig(params[0])) { + const { client, ...drizzleConfig } = params[0] as ( + & { client: BigQuery } + & DrizzleConfig + ); + + return construct(client, drizzleConfig); + } + + return construct(params[0] as BigQuery, params[1] as DrizzleConfig | undefined); +} + +export namespace drizzle { + export function mock = Record>( + config?: DrizzleConfig, + ): NodeBigQueryDatabase & { + $client: '$client is not available on drizzle.mock()'; + } { + return construct({} as any, config) as any; + } +} diff --git a/drizzle-orm/src/bigquery/index.ts b/drizzle-orm/src/bigquery/index.ts new file mode 100644 index 0000000000..b1b6a52e71 --- /dev/null +++ b/drizzle-orm/src/bigquery/index.ts @@ -0,0 +1,2 @@ +export * from './driver.ts'; +export * from './session.ts'; diff --git a/drizzle-orm/src/bigquery/migrator.ts b/drizzle-orm/src/bigquery/migrator.ts new file mode 100644 index 0000000000..e455075145 --- /dev/null +++ b/drizzle-orm/src/bigquery/migrator.ts @@ -0,0 +1,11 @@ +import type { MigrationConfig } from '~/migrator.ts'; +import { readMigrationFiles } from '~/migrator.ts'; +import type { NodeBigQueryDatabase } from './driver.ts'; + +export async function migrate>( + db: NodeBigQueryDatabase, + config: MigrationConfig, +) { + const migrations = readMigrationFiles(config); + await db.dialect.migrate(migrations, db.session, config); +} diff --git a/drizzle-orm/src/bigquery/session.ts b/drizzle-orm/src/bigquery/session.ts new file mode 100644 index 0000000000..5788717a95 --- /dev/null +++ b/drizzle-orm/src/bigquery/session.ts @@ -0,0 +1,217 @@ +import type { BigQuery, Query, QueryRowsResponse } from '@google-cloud/bigquery'; +import type { BigQueryDialect } from '~/bigquery-core/dialect.ts'; +import { BigQueryDatabase, BigQueryTransaction } from '~/bigquery-core/index.ts'; +import type { SelectedFieldsOrdered } from '~/bigquery-core/query-builders/select.types.ts'; +import type { BigQueryTransactionConfig, PreparedQueryConfig } from '~/bigquery-core/session.ts'; +import { BigQueryPreparedQuery, BigQuerySession } from '~/bigquery-core/session.ts'; +import { Column } from '~/column.ts'; +import { entityKind, is } from '~/entity.ts'; +import { type Logger, NoopLogger } from '~/logger.ts'; +import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; +import { fillPlaceholders, type Query as DrizzleQuery, SQL, sql } from '~/sql/sql.ts'; +import { tracer } from '~/tracing.ts'; +import { mapResultRow } from '~/utils.ts'; + +export class BigQueryClientPreparedQuery extends BigQueryPreparedQuery { + static override readonly [entityKind]: string = 'BigQueryClientPreparedQuery'; + + constructor( + private client: BigQuery, + private queryString: string, + private params: unknown[], + private logger: Logger, + private fields: SelectedFieldsOrdered | undefined, + private _isResponseInArrayMode: boolean, + private customResultMapper?: (rows: unknown[][]) => T['execute'], + ) { + super({ sql: queryString, params }); + } + + async execute(placeholderValues: Record | undefined = {}): Promise { + return tracer.startActiveSpan('drizzle.execute', async () => { + const params = fillPlaceholders(this.params, placeholderValues); + + this.logger.logQuery(this.queryString, params); + + const { fields, client, queryString, joinsNotNullableMap, customResultMapper } = this; + + const options = { + query: queryString, + params: params, + }; + + if (!fields && !customResultMapper) { + return tracer.startActiveSpan('drizzle.driver.execute', async (span) => { + span?.setAttributes({ + 'drizzle.query.text': queryString, + 'drizzle.query.params': JSON.stringify(params), + }); + const [rows] = await client.query(options); + return rows; + }); + } + + const result = await tracer.startActiveSpan('drizzle.driver.execute', async (span) => { + span?.setAttributes({ + 'drizzle.query.text': queryString, + 'drizzle.query.params': JSON.stringify(params), + }); + const [rows] = await client.query(options); + return rows; + }); + + return tracer.startActiveSpan('drizzle.mapResponse', () => { + if (customResultMapper) { + return customResultMapper(result as unknown[][]); + } + return result.map((row: Record) => { + // BigQuery returns objects with keys in SELECT order + // For columns, use the column name; for SQL expressions without aliases, + // BigQuery uses auto-generated names like f0_, f1_, etc. + const rowKeys = Object.keys(row); + const values = fields!.map((f, index) => { + const field = f.field; + let key: string; + if (is(field, Column)) { + key = field.name; + } else if (is(field, SQL.Aliased)) { + key = field.fieldAlias; + } else { + // For unaliased SQL expressions, BigQuery uses positional keys like f0_, f1_, etc. + // Try the path name first, then fall back to positional key + const pathKey = f.path[f.path.length - 1]!; + if (pathKey in row) { + key = pathKey; + } else { + // Use positional key - BigQuery names unaliased columns as f0_, f1_, etc. + key = rowKeys[index] ?? `f${index}_`; + } + } + return row[key]; + }); + return mapResultRow(fields!, values, joinsNotNullableMap); + }); + }); + }); + } + + all(placeholderValues: Record | undefined = {}): Promise { + return tracer.startActiveSpan('drizzle.execute', async () => { + const params = fillPlaceholders(this.params, placeholderValues); + this.logger.logQuery(this.queryString, params); + return tracer.startActiveSpan('drizzle.driver.execute', async (span) => { + span?.setAttributes({ + 'drizzle.query.text': this.queryString, + 'drizzle.query.params': JSON.stringify(params), + }); + const [rows] = await this.client.query({ + query: this.queryString, + params: params, + }); + return rows; + }); + }); + } + + /** @internal */ + isResponseInArrayMode(): boolean { + return this._isResponseInArrayMode; + } +} + +export interface BigQuerySessionOptions { + logger?: Logger; +} + +export class BigQueryClientSession< + TFullSchema extends Record, + TSchema extends TablesRelationalConfig, +> extends BigQuerySession { + static override readonly [entityKind]: string = 'BigQueryClientSession'; + + private logger: Logger; + + constructor( + private client: BigQuery, + dialect: BigQueryDialect, + private schema: RelationalSchemaConfig | undefined, + private options: BigQuerySessionOptions = {}, + ) { + super(dialect); + this.logger = options.logger ?? new NoopLogger(); + } + + prepareQuery( + query: DrizzleQuery, + fields: SelectedFieldsOrdered | undefined, + name: string | undefined, + isResponseInArrayMode: boolean, + customResultMapper?: (rows: unknown[][]) => T['execute'], + ): BigQueryPreparedQuery { + return new BigQueryClientPreparedQuery( + this.client, + query.sql, + query.params, + this.logger, + fields, + isResponseInArrayMode, + customResultMapper, + ); + } + + override async transaction( + transaction: (tx: BigQueryClientTransaction) => Promise, + _config?: BigQueryTransactionConfig, + ): Promise { + const tx = new BigQueryClientTransaction( + this.dialect, + this, + this.schema, + ); + + // BigQuery uses multi-statement transactions + await this.execute(sql`BEGIN TRANSACTION`); + try { + const result = await transaction(tx); + await this.execute(sql`COMMIT TRANSACTION`); + return result; + } catch (error) { + await this.execute(sql`ROLLBACK TRANSACTION`); + throw error; + } + } + + override async count(sqlQuery: import('~/sql/sql.ts').SQL): Promise { + const res = await this.execute<{ f0_: number }[]>(sqlQuery); + return Number(res[0]?.['f0_'] ?? 0); + } +} + +export class BigQueryClientTransaction< + TFullSchema extends Record, + TSchema extends TablesRelationalConfig, +> extends BigQueryTransaction { + static override readonly [entityKind]: string = 'BigQueryClientTransaction'; + + override async transaction( + transaction: (tx: BigQueryClientTransaction) => Promise, + ): Promise { + // BigQuery doesn't support nested transactions/savepoints in the same way as other databases + // For now, just execute the nested transaction in the same context + const tx = new BigQueryClientTransaction( + this.dialect, + this.session, + this.schema, + this.nestedIndex + 1, + ); + return await transaction(tx); + } +} + +export interface BigQueryQueryResultHKT { + readonly $brand: 'BigQueryQueryResultHKT'; + readonly row: unknown; + readonly type: unknown[]; +} + +export type BigQueryQueryResult = T[]; diff --git a/drizzle-orm/src/column-builder.ts b/drizzle-orm/src/column-builder.ts index 1cc4c5ae1e..fe9da6c3aa 100644 --- a/drizzle-orm/src/column-builder.ts +++ b/drizzle-orm/src/column-builder.ts @@ -1,4 +1,5 @@ import { entityKind } from '~/entity.ts'; +import type { BigQueryColumn } from './bigquery-core/index.ts'; import type { Column } from './column.ts'; import type { GelColumn, GelExtraConfigColumn } from './gel-core/index.ts'; import type { MySqlColumn } from './mysql-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' | 'bigquery'; export type GeneratedStorageMode = 'virtual' | 'stored'; @@ -368,6 +369,11 @@ export type BuildColumn< {}, Simplify | 'brand' | 'dialect'>> > + : TDialect extends 'bigquery' ? BigQueryColumn< + MakeColumnConfig, + {}, + Simplify | 'brand' | 'dialect'>> + > : never; export type BuildIndexColumn< @@ -413,4 +419,5 @@ export type ChangeColumnTableName> : TDialect extends 'sqlite' ? SQLiteColumn> : TDialect extends 'gel' ? GelColumn> + : TDialect extends 'bigquery' ? BigQueryColumn> : never; diff --git a/integration-tests/package.json b/integration-tests/package.json index 271cb7d49c..dea535d983 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -16,6 +16,7 @@ "private": true, "devDependencies": { "@cloudflare/workers-types": "^4.20241004.0", + "@google-cloud/bigquery": "^8.1.1", "@libsql/client": "^0.10.0", "@neondatabase/serverless": "0.10.0", "@originjs/vite-plugin-commonjs": "^1.0.3", @@ -32,8 +33,8 @@ "@vitest/ui": "^1.6.0", "ava": "^5.3.0", "cross-env": "^7.0.3", - "keyv": "^5.2.3", "import-in-the-middle": "^1.13.1", + "keyv": "^5.2.3", "ts-node": "^10.9.2", "tsx": "^4.14.0", "vite-tsconfig-paths": "^4.3.2", diff --git a/integration-tests/tests/bigquery/bigquery-common.ts b/integration-tests/tests/bigquery/bigquery-common.ts new file mode 100644 index 0000000000..1fc37be0a2 --- /dev/null +++ b/integration-tests/tests/bigquery/bigquery-common.ts @@ -0,0 +1,548 @@ +import { + and, + asc, + avg, + count, + countDistinct, + desc, + eq, + gt, + gte, + inArray, + lt, + lte, + max, + min, + ne, + not, + or, + sql, + sum, +} from 'drizzle-orm'; +import type { BigQueryDatabase } from 'drizzle-orm/bigquery-core'; +import { bigqueryTable, bool, float64, int64, string, timestamp } from 'drizzle-orm/bigquery-core'; +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; + +declare module 'vitest' { + interface TestContext { + bigquery: { + db: BigQueryDatabase; + }; + } +} + +// Test table schemas - these will be created in the test dataset +export const usersTable = bigqueryTable('drizzle_test.users', { + id: string('id').notNull(), + name: string('name').notNull(), + email: string('email'), + verified: bool('verified').default(false), + createdAt: timestamp('created_at'), +}); + +export const ordersTable = bigqueryTable('drizzle_test.orders', { + id: string('id').notNull(), + userId: string('user_id').notNull(), + product: string('product').notNull(), + quantity: int64('quantity').notNull(), + price: float64('price').notNull(), + createdAt: timestamp('created_at'), +}); + +export const aggregateTable = bigqueryTable('drizzle_test.aggregate_table', { + id: int64('id').notNull(), + name: string('name').notNull(), + a: int64('a'), + b: int64('b'), + c: int64('c'), + nullOnly: int64('null_only'), +}); + +// Helper to generate unique IDs (BigQuery doesn't have auto-increment) +function generateId(): string { + return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; +} + +export function tests() { + describe('common', () => { + beforeEach(async (ctx) => { + const { db } = ctx.bigquery; + + // Clean up tables before each test + await db.execute(sql`DELETE FROM drizzle_test.users WHERE 1=1`); + await db.execute(sql`DELETE FROM drizzle_test.orders WHERE 1=1`); + await db.execute(sql`DELETE FROM drizzle_test.aggregate_table WHERE 1=1`); + }); + + test('select all fields', async (ctx) => { + const { db } = ctx.bigquery; + + const id = generateId(); + await db.insert(usersTable).values({ + id, + name: 'John', + email: 'john@example.com', + verified: true, + }); + + const result = await db.select().from(usersTable).where(eq(usersTable.id, id)); + + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('John'); + expect(result[0]!.email).toBe('john@example.com'); + expect(result[0]!.verified).toBe(true); + }); + + test('select specific columns', async (ctx) => { + const { db } = ctx.bigquery; + + const id = generateId(); + await db.insert(usersTable).values({ + id, + name: 'Jane', + email: 'jane@example.com', + }); + + const result = await db + .select({ + id: usersTable.id, + name: usersTable.name, + }) + .from(usersTable) + .where(eq(usersTable.id, id)); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ id, name: 'Jane' }); + }); + + test('select with where eq', async (ctx) => { + const { db } = ctx.bigquery; + + const id1 = generateId(); + const id2 = generateId(); + + await db.insert(usersTable).values([ + { id: id1, name: 'John' }, + { id: id2, name: 'Jane' }, + ]); + + const result = await db + .select() + .from(usersTable) + .where(eq(usersTable.name, 'John')); + + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('John'); + }); + + test('select with where ne', async (ctx) => { + const { db } = ctx.bigquery; + + const id1 = generateId(); + const id2 = generateId(); + + await db.insert(usersTable).values([ + { id: id1, name: 'John' }, + { id: id2, name: 'Jane' }, + ]); + + const result = await db + .select() + .from(usersTable) + .where(ne(usersTable.name, 'John')); + + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('Jane'); + }); + + test('select with where and', async (ctx) => { + const { db } = ctx.bigquery; + + const id1 = generateId(); + const id2 = generateId(); + + await db.insert(usersTable).values([ + { id: id1, name: 'John', verified: true }, + { id: id2, name: 'Jane', verified: false }, + ]); + + const result = await db + .select() + .from(usersTable) + .where(and(eq(usersTable.name, 'John'), eq(usersTable.verified, true))); + + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('John'); + }); + + test('select with where or', async (ctx) => { + const { db } = ctx.bigquery; + + const id1 = generateId(); + const id2 = generateId(); + const id3 = generateId(); + + await db.insert(usersTable).values([ + { id: id1, name: 'John' }, + { id: id2, name: 'Jane' }, + { id: id3, name: 'Jack' }, + ]); + + const result = await db + .select() + .from(usersTable) + .where(or(eq(usersTable.name, 'John'), eq(usersTable.name, 'Jane'))); + + expect(result).toHaveLength(2); + }); + + test('select with order by asc', async (ctx) => { + const { db } = ctx.bigquery; + + const id1 = generateId(); + const id2 = generateId(); + const id3 = generateId(); + + await db.insert(usersTable).values([ + { id: id1, name: 'Charlie' }, + { id: id2, name: 'Alice' }, + { id: id3, name: 'Bob' }, + ]); + + const result = await db + .select() + .from(usersTable) + .orderBy(asc(usersTable.name)); + + expect(result).toHaveLength(3); + expect(result[0]!.name).toBe('Alice'); + expect(result[1]!.name).toBe('Bob'); + expect(result[2]!.name).toBe('Charlie'); + }); + + test('select with order by desc', async (ctx) => { + const { db } = ctx.bigquery; + + const id1 = generateId(); + const id2 = generateId(); + const id3 = generateId(); + + await db.insert(usersTable).values([ + { id: id1, name: 'Charlie' }, + { id: id2, name: 'Alice' }, + { id: id3, name: 'Bob' }, + ]); + + const result = await db + .select() + .from(usersTable) + .orderBy(desc(usersTable.name)); + + expect(result).toHaveLength(3); + expect(result[0]!.name).toBe('Charlie'); + expect(result[1]!.name).toBe('Bob'); + expect(result[2]!.name).toBe('Alice'); + }); + + test('select with limit', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(usersTable).values([ + { id: generateId(), name: 'User1' }, + { id: generateId(), name: 'User2' }, + { id: generateId(), name: 'User3' }, + ]); + + const result = await db + .select() + .from(usersTable) + .limit(2); + + expect(result).toHaveLength(2); + }); + + test('select with limit and offset', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(usersTable).values([ + { id: generateId(), name: 'User1' }, + { id: generateId(), name: 'User2' }, + { id: generateId(), name: 'User3' }, + ]); + + const result = await db + .select() + .from(usersTable) + .orderBy(asc(usersTable.name)) + .limit(1) + .offset(1); + + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('User2'); + }); + + test('select with inArray', async (ctx) => { + const { db } = ctx.bigquery; + + const id1 = generateId(); + const id2 = generateId(); + const id3 = generateId(); + + await db.insert(usersTable).values([ + { id: id1, name: 'John' }, + { id: id2, name: 'Jane' }, + { id: id3, name: 'Jack' }, + ]); + + const result = await db + .select() + .from(usersTable) + .where(inArray(usersTable.name, ['John', 'Jane'])); + + expect(result).toHaveLength(2); + }); + + test('insert single row', async (ctx) => { + const { db } = ctx.bigquery; + + const id = generateId(); + await db.insert(usersTable).values({ + id, + name: 'Test User', + email: 'test@example.com', + }); + + const result = await db + .select() + .from(usersTable) + .where(eq(usersTable.id, id)); + + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('Test User'); + }); + + test('insert multiple rows', async (ctx) => { + const { db } = ctx.bigquery; + + const id1 = generateId(); + const id2 = generateId(); + + await db.insert(usersTable).values([ + { id: id1, name: 'User 1' }, + { id: id2, name: 'User 2' }, + ]); + + const result = await db + .select() + .from(usersTable) + .where(inArray(usersTable.id, [id1, id2])); + + expect(result).toHaveLength(2); + }); + + test('update', async (ctx) => { + const { db } = ctx.bigquery; + + const id = generateId(); + await db.insert(usersTable).values({ + id, + name: 'Original Name', + }); + + await db + .update(usersTable) + .set({ name: 'Updated Name' }) + .where(eq(usersTable.id, id)); + + const result = await db + .select() + .from(usersTable) + .where(eq(usersTable.id, id)); + + expect(result[0]!.name).toBe('Updated Name'); + }); + + test('delete', async (ctx) => { + const { db } = ctx.bigquery; + + const id = generateId(); + await db.insert(usersTable).values({ + id, + name: 'To Delete', + }); + + // Verify inserted + const beforeDelete = await db + .select() + .from(usersTable) + .where(eq(usersTable.id, id)); + expect(beforeDelete).toHaveLength(1); + + await db.delete(usersTable).where(eq(usersTable.id, id)); + + const afterDelete = await db + .select() + .from(usersTable) + .where(eq(usersTable.id, id)); + expect(afterDelete).toHaveLength(0); + }); + + test('count aggregate', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(usersTable).values([ + { id: generateId(), name: 'User1' }, + { id: generateId(), name: 'User2' }, + { id: generateId(), name: 'User3' }, + ]); + + const result = await db + .select({ + count: count(), + }) + .from(usersTable); + + expect(result[0]!.count).toBe(3); + }); + + test('count with group by', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(usersTable).values([ + { id: generateId(), name: 'John', verified: true }, + { id: generateId(), name: 'Jane', verified: true }, + { id: generateId(), name: 'Jack', verified: false }, + ]); + + const result = await db + .select({ + verified: usersTable.verified, + count: count(), + }) + .from(usersTable) + .groupBy(usersTable.verified) + .orderBy(desc(count())); + + expect(result).toHaveLength(2); + }); + + test('sum aggregate', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(ordersTable).values([ + { id: generateId(), userId: 'u1', product: 'A', quantity: 2, price: 10.0 }, + { id: generateId(), userId: 'u1', product: 'B', quantity: 3, price: 20.0 }, + ]); + + const result = await db + .select({ + totalQuantity: sum(ordersTable.quantity), + }) + .from(ordersTable); + + expect(Number(result[0]!.totalQuantity)).toBe(5); + }); + + test('avg aggregate', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(ordersTable).values([ + { id: generateId(), userId: 'u1', product: 'A', quantity: 10, price: 10.0 }, + { id: generateId(), userId: 'u1', product: 'B', quantity: 20, price: 20.0 }, + ]); + + const result = await db + .select({ + avgQuantity: avg(ordersTable.quantity), + }) + .from(ordersTable); + + expect(Number(result[0]!.avgQuantity)).toBe(15); + }); + + test('min and max aggregate', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(ordersTable).values([ + { id: generateId(), userId: 'u1', product: 'A', quantity: 5, price: 10.0 }, + { id: generateId(), userId: 'u1', product: 'B', quantity: 15, price: 20.0 }, + { id: generateId(), userId: 'u1', product: 'C', quantity: 10, price: 30.0 }, + ]); + + const result = await db + .select({ + minQuantity: min(ordersTable.quantity), + maxQuantity: max(ordersTable.quantity), + }) + .from(ordersTable); + + expect(result[0]!.minQuantity).toBe(5); + expect(result[0]!.maxQuantity).toBe(15); + }); + + test('raw sql in select', async (ctx) => { + const { db } = ctx.bigquery; + + const id = generateId(); + await db.insert(usersTable).values({ + id, + name: 'John', + }); + + const result = await db + .select({ + name: usersTable.name, + upperName: sql`UPPER(${usersTable.name})`.as('upper_name'), + }) + .from(usersTable) + .where(eq(usersTable.id, id)); + + expect(result[0]!.upperName).toBe('JOHN'); + }); + + test('raw sql in where', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(usersTable).values([ + { id: generateId(), name: 'John' }, + { id: generateId(), name: 'JOHN' }, + ]); + + const result = await db + .select() + .from(usersTable) + .where(sql`LOWER(${usersTable.name}) = 'john'`); + + expect(result).toHaveLength(2); + }); + + test('execute raw sql', async (ctx) => { + const { db } = ctx.bigquery; + + const result = await db.execute(sql`SELECT 1 + 1 as result`); + + expect(result[0]).toEqual({ result: 2 }); + }); + + test('distinct select', async (ctx) => { + const { db } = ctx.bigquery; + + await db.insert(usersTable).values([ + { id: generateId(), name: 'John' }, + { id: generateId(), name: 'John' }, + { id: generateId(), name: 'Jane' }, + ]); + + const result = await db + .selectDistinct({ + name: usersTable.name, + }) + .from(usersTable) + .orderBy(asc(usersTable.name)); + + expect(result).toHaveLength(2); + expect(result[0]!.name).toBe('Jane'); + expect(result[1]!.name).toBe('John'); + }); + }); +} diff --git a/integration-tests/tests/bigquery/bigquery.test.ts b/integration-tests/tests/bigquery/bigquery.test.ts new file mode 100644 index 0000000000..e4d01335c9 --- /dev/null +++ b/integration-tests/tests/bigquery/bigquery.test.ts @@ -0,0 +1,143 @@ +import { BigQuery } from '@google-cloud/bigquery'; +import { sql } from 'drizzle-orm'; +import type { NodeBigQueryDatabase } from 'drizzle-orm/bigquery'; +import { drizzle } from 'drizzle-orm/bigquery'; +import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'; +import { aggregateTable, ordersTable, tests, usersTable } from './bigquery-common'; + +const ENABLE_LOGGING = false; + +// BigQuery project and dataset configuration - required environment variables +const PROJECT_ID = process.env['BIGQUERY_PROJECT_ID']; +const DATASET_ID = process.env['BIGQUERY_DATASET_ID'] || 'drizzle_test'; + +if (!PROJECT_ID) { + throw new Error('BIGQUERY_PROJECT_ID environment variable is required'); +} + +let db: NodeBigQueryDatabase; +let bigquery: BigQuery; + +beforeAll(async () => { + bigquery = new BigQuery({ + projectId: PROJECT_ID, + }); + + db = drizzle(bigquery, { logger: ENABLE_LOGGING }); + + // Create test dataset if it doesn't exist + try { + await bigquery.createDataset(DATASET_ID, { location: 'US' }); + console.log(`Created dataset ${DATASET_ID}`); + } catch (error: any) { + if (error.code !== 409) { + // 409 = Already exists + throw error; + } + } + + // Create test tables + await createTestTables(); +}); + +afterAll(async () => { + // Optionally clean up test dataset + // await bigquery.dataset(DATASET_ID).delete({ force: true }); +}); + +beforeEach((ctx) => { + ctx.bigquery = { + db, + }; +}); + +async function createTestTables() { + const dataset = bigquery.dataset(DATASET_ID); + + // Users table + try { + await dataset.createTable('users', { + schema: { + fields: [ + { name: 'id', type: 'STRING', mode: 'REQUIRED' }, + { name: 'name', type: 'STRING', mode: 'REQUIRED' }, + { name: 'email', type: 'STRING', mode: 'NULLABLE' }, + { name: 'verified', type: 'BOOL', mode: 'NULLABLE' }, + { name: 'created_at', type: 'TIMESTAMP', mode: 'NULLABLE' }, + ], + }, + }); + console.log('Created users table'); + } catch (error: any) { + if (error.code !== 409) throw error; + } + + // Orders table + try { + await dataset.createTable('orders', { + schema: { + fields: [ + { name: 'id', type: 'STRING', mode: 'REQUIRED' }, + { name: 'user_id', type: 'STRING', mode: 'REQUIRED' }, + { name: 'product', type: 'STRING', mode: 'REQUIRED' }, + { name: 'quantity', type: 'INT64', mode: 'REQUIRED' }, + { name: 'price', type: 'FLOAT64', mode: 'REQUIRED' }, + { name: 'created_at', type: 'TIMESTAMP', mode: 'NULLABLE' }, + ], + }, + }); + console.log('Created orders table'); + } catch (error: any) { + if (error.code !== 409) throw error; + } + + // Aggregate test table + try { + await dataset.createTable('aggregate_table', { + schema: { + fields: [ + { name: 'id', type: 'INT64', mode: 'REQUIRED' }, + { name: 'name', type: 'STRING', mode: 'REQUIRED' }, + { name: 'a', type: 'INT64', mode: 'NULLABLE' }, + { name: 'b', type: 'INT64', mode: 'NULLABLE' }, + { name: 'c', type: 'INT64', mode: 'NULLABLE' }, + { name: 'null_only', type: 'INT64', mode: 'NULLABLE' }, + ], + }, + }); + console.log('Created aggregate_table'); + } catch (error: any) { + if (error.code !== 409) throw error; + } +} + +// BigQuery-specific tests +test('drizzle instance has $client', () => { + expect(db.$client).toBe(bigquery); +}); + +test('execute raw query', async () => { + const result = await db.execute(sql`SELECT 1 + 1 as result`); + expect(result).toEqual([{ result: 2 }]); +}); + +test('toSQL returns correct BigQuery syntax', () => { + const query = db.select().from(usersTable).where(sql`${usersTable.name} = 'John'`); + const sqlResult = query.toSQL(); + + // BigQuery uses backticks for identifiers + expect(sqlResult.sql).toContain('`'); + expect(sqlResult.sql).toContain('drizzle_test.users'); +}); + +test('parameters use positional placeholders', () => { + const query = db.select().from(usersTable).limit(10); + const sqlResult = query.toSQL(); + + // BigQuery uses ? for positional parameters + expect(sqlResult.sql).toContain('?'); + expect(sqlResult.params).toContain(10); +}); + +// Run the common tests +tests(); diff --git a/integration-tests/vitest.config.ts b/integration-tests/vitest.config.ts index 3751e53e35..bfffbd82dd 100644 --- a/integration-tests/vitest.config.ts +++ b/integration-tests/vitest.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ 'tests/mysql/**/*.test.ts', 'tests/singlestore/**/*.test.ts', 'tests/sqlite/**/*.test.ts', + 'tests/bigquery/**/*.test.ts', 'tests/replicas/**/*', 'tests/imports/**/*', 'tests/extensions/vectors/**/*', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 035f757a52..df00b0f88f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 5.2.2(prettier@3.5.3) '@typescript-eslint/eslint-plugin': specifier: ^6.7.3 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.6.3) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/experimental-utils': specifier: ^5.62.0 version: 5.62.0(eslint@8.57.1)(typescript@5.6.3) @@ -40,7 +40,7 @@ importers: version: link:drizzle-orm/dist drizzle-orm-old: specifier: npm:drizzle-orm@^0.27.2 - version: drizzle-orm@0.27.2(bun-types@1.2.15) + version: drizzle-orm@0.27.2(@aws-sdk/client-rds-data@3.817.0)(@cloudflare/workers-types@4.20250529.0)(@libsql/client@0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.2)(@types/sql.js@1.4.9)(@vercel/postgres@0.8.0)(better-sqlite3@11.10.0)(bun-types@1.2.15)(knex@2.5.1(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.0)(sqlite3@5.1.7))(kysely@0.25.0)(mysql2@3.14.1)(pg@8.16.0)(postgres@3.4.7)(sql.js@1.13.0)(sqlite3@5.1.7) eslint: specifier: ^8.50.0 version: 8.57.1 @@ -49,7 +49,7 @@ importers: version: link:eslint/eslint-plugin-drizzle-internal eslint-plugin-import: specifier: ^2.28.1 - version: 2.31.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1) + version: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1) eslint-plugin-no-instanceof: specifier: ^1.0.1 version: 1.0.1 @@ -58,7 +58,7 @@ importers: version: 48.0.1(eslint@8.57.1) eslint-plugin-unused-imports: specifier: ^3.0.0 - version: 3.2.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.1) + version: 3.2.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1) glob: specifier: ^10.3.10 version: 10.4.5 @@ -73,7 +73,7 @@ importers: version: 0.8.23(typescript@5.6.3) tsup: specifier: ^8.3.5 - version: 8.5.0(tsx@4.19.4)(typescript@5.6.3) + version: 8.5.0(postcss@8.5.4)(tsx@4.19.4)(typescript@5.6.3)(yaml@2.8.0) tsx: specifier: ^4.10.5 version: 4.19.4 @@ -118,10 +118,10 @@ importers: version: 4.19.4 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3) + version: 4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0)) vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@18.19.108) + version: 3.1.4(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) zx: specifier: ^7.2.2 version: 7.2.3 @@ -161,7 +161,7 @@ importers: version: 0.2.2(hono@4.7.10)(zod@3.25.42) '@libsql/client': specifier: ^0.10.0 - version: 0.10.0 + version: 0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@neondatabase/serverless': specifier: ^0.9.1 version: 0.9.5 @@ -209,7 +209,7 @@ importers: version: 8.18.1 '@typescript-eslint/eslint-plugin': specifier: ^7.2.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0)(eslint@8.57.1)(typescript@5.6.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/parser': specifier: ^7.2.0 version: 7.18.0(eslint@8.57.1)(typescript@5.6.3) @@ -260,7 +260,7 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.4.1(eslint-config-prettier@9.1.0)(eslint@8.57.1)(prettier@3.5.3) + version: 5.4.1(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.5.3) gel: specifier: ^2.0.0 version: 2.1.0 @@ -314,7 +314,7 @@ importers: version: 2.2.2 tsup: specifier: ^8.3.5 - version: 8.5.0(tsx@3.14.0)(typescript@5.6.3) + version: 8.5.0(postcss@8.5.4)(tsx@3.14.0)(typescript@5.6.3)(yaml@2.8.0) tsx: specifier: ^3.12.1 version: 3.14.0 @@ -326,13 +326,13 @@ importers: version: 9.0.1 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3) + version: 4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0)) vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@18.19.108) + version: 3.1.4(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) ws: specifier: ^8.18.2 - version: 8.18.2 + version: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) zod: specifier: ^3.20.2 version: 3.25.42 @@ -351,9 +351,12 @@ importers: '@electric-sql/pglite': specifier: ^0.2.12 version: 0.2.12 + '@google-cloud/bigquery': + specifier: ^8.1.1 + version: 8.1.1 '@libsql/client': specifier: ^0.10.0 - version: 0.10.0 + version: 0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@libsql/client-wasm': specifier: ^0.10.0 version: 0.10.0 @@ -365,7 +368,7 @@ importers: version: 0.10.0 '@op-engineering/op-sqlite': specifier: ^2.0.16 - version: 2.0.22(react-native@0.79.2)(react@18.3.1) + version: 2.0.22(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) '@opentelemetry/api': specifier: ^1.4.1 version: 1.9.0 @@ -416,7 +419,7 @@ importers: version: 10.1.0 expo-sqlite: specifier: ^14.0.0 - version: 14.0.6(expo@53.0.9) + version: 14.0.6(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) gel: specifier: ^2.0.0 version: 2.1.0 @@ -461,10 +464,10 @@ importers: version: 3.14.0 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3) + version: 4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0)) vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1) + version: 3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1)(lightningcss@1.27.0)(terser@5.40.0) zod: specifier: ^3.20.2 version: 3.25.42 @@ -549,7 +552,7 @@ importers: version: 10.0.0 vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@22.15.27) + version: 3.1.4(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0) zx: specifier: ^8.1.5 version: 8.5.4 @@ -582,10 +585,10 @@ importers: version: 3.29.5 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3) + version: 4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0)) vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@18.19.108) + version: 3.1.4(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) zx: specifier: ^7.2.2 version: 7.2.3 @@ -618,10 +621,10 @@ importers: version: 1.0.0-beta.7(typescript@5.6.3) vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3) + version: 4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0)) vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@18.19.108) + version: 3.1.4(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) zx: specifier: ^7.2.2 version: 7.2.3 @@ -651,10 +654,10 @@ importers: version: 3.29.5 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3) + version: 4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0)) vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@18.19.108) + version: 3.1.4(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) zod: specifier: 3.25.1 version: 3.25.1 @@ -687,7 +690,7 @@ importers: version: 5.6.3 vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1) + version: 3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1)(lightningcss@1.27.0)(terser@5.40.0) integration-tests: dependencies: @@ -702,7 +705,7 @@ importers: version: 0.2.12 '@libsql/client': specifier: ^0.10.0 - version: 0.10.0 + version: 0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@miniflare/d1': specifier: ^2.14.4 version: 2.14.4 @@ -792,10 +795,10 @@ importers: version: 0.5.6 vitest: specifier: ^3.1.3 - version: 3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1) + version: 3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1)(lightningcss@1.27.0)(terser@5.40.0) ws: specifier: ^8.18.2 - version: 8.18.2 + version: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) zod: specifier: ^3.20.2 version: 3.25.42 @@ -803,6 +806,9 @@ importers: '@cloudflare/workers-types': specifier: ^4.20241004.0 version: 4.20250529.0 + '@google-cloud/bigquery': + specifier: ^8.1.1 + version: 8.1.1 '@neondatabase/serverless': specifier: 0.10.0 version: 0.10.0 @@ -862,7 +868,7 @@ importers: version: 4.19.4 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3) + version: 4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0)) zx: specifier: ^8.3.2 version: 8.5.4 @@ -2267,6 +2273,34 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@google-cloud/bigquery@8.1.1': + resolution: {integrity: sha512-2GHlohfA/VJffTvibMazMsZi6jPRx8MmaMberyDTL8rnhVs/frKSXVVRtLU83uSAy2j/5SD4mOs4jMQgJPON2g==} + engines: {node: '>=18'} + + '@google-cloud/common@6.0.0': + resolution: {integrity: sha512-IXh04DlkLMxWgYLIUYuHHKXKOUwPDzDgke1ykkkJPe48cGIS9kkL2U/o0pm4ankHLlvzLF/ma1eO86n/bkumIA==} + engines: {node: '>=18'} + + '@google-cloud/paginator@6.0.0': + resolution: {integrity: sha512-g5nmMnzC+94kBxOKkLGpK1ikvolTFCC3s2qtE4F+1EuArcJ7HHC23RDQVt3Ra3CqpUYZ+oXNKZ8n5Cn5yug8DA==} + engines: {node: '>=18'} + + '@google-cloud/precise-date@5.0.0': + resolution: {integrity: sha512-9h0Gvw92EvPdE8AK8AgZPbMnH5ftDyPtKm7/KUfcJVaPEPjwGDsJd1QV0H8esBDV4II41R/2lDWH1epBqIoKUw==} + engines: {node: '>=18'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.1.0': + resolution: {integrity: sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg==} + engines: {node: '>=18'} + + '@google-cloud/promisify@5.0.0': + resolution: {integrity: sha512-N8qS6dlORGHwk7WjGXKOSsLjIjNINCPicsOX6gyyLiYk7mq3MtII96NZ9N2ahwA2vnkLmZODOIH9rlNniYWvCQ==} + engines: {node: '>=18'} + '@grpc/grpc-js@1.13.4': resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} engines: {node: '>=12.10.0'} @@ -2449,6 +2483,7 @@ packages: '@miniflare/core@2.14.4': resolution: {integrity: sha512-FMmZcC1f54YpF4pDWPtdQPIO8NXfgUxCoR9uyrhxKJdZu7M6n8QKopPVNuaxR40jcsdxb7yKoQoFWnHfzJD9GQ==} engines: {node: '>=16.13'} + deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/d1@2.14.4': resolution: {integrity: sha512-pMBVq9XWxTDdm+RRCkfXZP+bREjPg1JC8s8C0JTovA9OGmLQXqGTnFxIaS9vf1d8k3uSUGhDzPTzHr0/AUW1gA==} @@ -2458,6 +2493,7 @@ packages: '@miniflare/queues@2.14.4': resolution: {integrity: sha512-aXQ5Ik8Iq1KGMBzGenmd6Js/jJgqyYvjom95/N9GptCGpiVWE5F0XqC1SL5rCwURbHN+aWY191o8XOFyY2nCUA==} engines: {node: '>=16.7'} + deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/shared@2.14.4': resolution: {integrity: sha512-upl4RSB3hyCnITOFmRZjJj4A72GmkVrtfZTilkdq5Qe5TTlzsjVeDJp7AuNUM9bM8vswRo+N5jOiot6O4PVwwQ==} @@ -2467,6 +2503,7 @@ packages: '@miniflare/watcher@2.14.4': resolution: {integrity: sha512-PYn05ET2USfBAeXF6NZfWl0O32KVyE8ncQ/ngysrh3hoIV7l3qGGH7ubeFx+D8VWQ682qYhwGygUzQv2j1tGGg==} engines: {node: '>=16.13'} + deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@modelcontextprotocol/sdk@1.6.1': resolution: {integrity: sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA==} @@ -2996,6 +3033,10 @@ packages: resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + '@trivago/prettier-plugin-sort-imports@5.2.2': resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==} engines: {node: '>18.12'} @@ -3576,6 +3617,10 @@ packages: resolution: {integrity: sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==} engines: {node: '>=8.0.0'} + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + arrify@3.0.0: resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} engines: {node: '>=12'} @@ -3709,6 +3754,12 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + big.js@6.2.2: + resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -3758,6 +3809,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -4439,9 +4493,15 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -4994,6 +5054,9 @@ packages: ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -5159,6 +5222,14 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + gaxios@7.1.3: + resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + gel@2.1.0: resolution: {integrity: sha512-HCeRqInCt6BjbMmeghJ6BKeYwOj7WJT5Db6IWWAA3IMUUa7or7zJfTUEkUWCxiOtoXnwnm96sFK9Fr47Yh2hOA==} engines: {node: '>= 18.0.0'} @@ -5276,6 +5347,14 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + google-auth-library@10.5.0: + resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -5286,6 +5365,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gtoken@8.0.0: + resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} + engines: {node: '>=18'} + hanji@0.0.5: resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} @@ -5359,6 +5442,9 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -5370,6 +5456,10 @@ packages: resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} engines: {node: '>= 6'} + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -5771,6 +5861,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -5828,6 +5921,12 @@ packages: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -7168,6 +7267,10 @@ packages: resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} engines: {node: '>=4'} + retry-request@8.0.2: + resolution: {integrity: sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==} + engines: {node: '>=18'} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -7419,6 +7522,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions spawn-command@0.0.2: resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} @@ -7459,9 +7563,6 @@ packages: sqlite3@5.1.7: resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} - peerDependenciesMeta: - node-gyp: - optional: true sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} @@ -7555,6 +7656,12 @@ packages: stream-combiner@0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -7620,6 +7727,9 @@ packages: structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -7680,6 +7790,10 @@ packages: resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} engines: {node: '>=8.0.0'} + teeny-request@10.1.0: + resolution: {integrity: sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==} + engines: {node: '>=18'} + temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -9898,7 +10012,7 @@ snapshots: dependencies: heap: 0.2.7 - '@expo/cli@0.24.13': + '@expo/cli@0.24.13(bufferutil@4.0.8)(utf-8-validate@6.0.3)': dependencies: '@0no-co/graphql.web': 1.1.2 '@babel/runtime': 7.27.3 @@ -9917,7 +10031,7 @@ snapshots: '@expo/spawn-async': 1.7.2 '@expo/ws-tunnel': 1.0.6 '@expo/xcpretty': 4.3.2 - '@react-native/dev-middleware': 0.79.2 + '@react-native/dev-middleware': 0.79.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@urql/core': 5.1.1 '@urql/exchange-retry': 1.3.1(@urql/core@5.1.1) accepts: 1.3.8 @@ -9960,7 +10074,7 @@ snapshots: terminal-link: 2.1.1 undici: 6.21.3 wrap-ansi: 7.0.0 - ws: 8.18.2 + ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - graphql @@ -10128,11 +10242,11 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/vector-icons@14.1.0(expo-font@13.3.1)(react-native@0.79.2)(react@18.3.1)': + '@expo/vector-icons@14.1.0(expo-font@13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)': dependencies: - expo-font: 13.3.1(expo@53.0.9)(react@18.3.1) + expo-font: 13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) react: 18.3.1 - react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1) + react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) '@expo/websql@1.0.1': dependencies: @@ -10156,6 +10270,47 @@ snapshots: '@gar/promisify@1.1.3': optional: true + '@google-cloud/bigquery@8.1.1': + dependencies: + '@google-cloud/common': 6.0.0 + '@google-cloud/paginator': 6.0.0 + '@google-cloud/precise-date': 5.0.0 + '@google-cloud/promisify': 5.0.0 + arrify: 3.0.0 + big.js: 6.2.2 + duplexify: 4.1.3 + extend: 3.0.2 + stream-events: 1.0.5 + teeny-request: 10.1.0 + transitivePeerDependencies: + - supports-color + + '@google-cloud/common@6.0.0': + dependencies: + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.1.0 + arrify: 2.0.1 + duplexify: 4.1.3 + extend: 3.0.2 + google-auth-library: 10.5.0 + html-entities: 2.6.0 + retry-request: 8.0.2 + teeny-request: 10.1.0 + transitivePeerDependencies: + - supports-color + + '@google-cloud/paginator@6.0.0': + dependencies: + extend: 3.0.2 + + '@google-cloud/precise-date@5.0.0': {} + + '@google-cloud/projectify@4.0.0': {} + + '@google-cloud/promisify@4.1.0': {} + + '@google-cloud/promisify@5.0.0': {} + '@grpc/grpc-js@1.13.4': dependencies: '@grpc/proto-loader': 0.7.15 @@ -10315,10 +10470,10 @@ snapshots: '@libsql/core': 0.10.0 js-base64: 3.7.7 - '@libsql/client@0.10.0': + '@libsql/client@0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)': dependencies: '@libsql/core': 0.10.0 - '@libsql/hrana-client': 0.6.2 + '@libsql/hrana-client': 0.6.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) js-base64: 3.7.7 libsql: 0.4.7 promise-limit: 2.7.0 @@ -10336,10 +10491,10 @@ snapshots: '@libsql/darwin-x64@0.4.7': optional: true - '@libsql/hrana-client@0.6.2': + '@libsql/hrana-client@0.6.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)': dependencies: '@libsql/isomorphic-fetch': 0.2.5 - '@libsql/isomorphic-ws': 0.1.5 + '@libsql/isomorphic-ws': 0.1.5(bufferutil@4.0.8)(utf-8-validate@6.0.3) js-base64: 3.7.7 node-fetch: 3.3.2 transitivePeerDependencies: @@ -10348,10 +10503,10 @@ snapshots: '@libsql/isomorphic-fetch@0.2.5': {} - '@libsql/isomorphic-ws@0.1.5': + '@libsql/isomorphic-ws@0.1.5(bufferutil@4.0.8)(utf-8-validate@6.0.3)': dependencies: '@types/ws': 8.18.1 - ws: 8.18.2 + ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -10458,10 +10613,10 @@ snapshots: rimraf: 3.0.2 optional: true - '@op-engineering/op-sqlite@2.0.22(react-native@0.79.2)(react@18.3.1)': + '@op-engineering/op-sqlite@2.0.22(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)': dependencies: react: 18.3.1 - react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1) + react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) '@opentelemetry/api@1.9.0': {} @@ -10490,7 +10645,7 @@ snapshots: prettier: 3.5.3 '@prisma/client@5.14.0(prisma@5.14.0)': - dependencies: + optionalDependencies: prisma: 5.14.0 '@prisma/debug@5.14.0': {} @@ -10612,14 +10767,14 @@ snapshots: nullthrows: 1.1.1 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.79.2': + '@react-native/community-cli-plugin@0.79.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)': dependencies: - '@react-native/dev-middleware': 0.79.2 + '@react-native/dev-middleware': 0.79.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) chalk: 4.1.2 debug: 2.6.9 invariant: 2.2.4 - metro: 0.82.4 - metro-config: 0.82.4 + metro: 0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3) + metro-config: 0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3) metro-core: 0.82.4 semver: 7.7.2 transitivePeerDependencies: @@ -10629,7 +10784,7 @@ snapshots: '@react-native/debugger-frontend@0.79.2': {} - '@react-native/dev-middleware@0.79.2': + '@react-native/dev-middleware@0.79.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)': dependencies: '@isaacs/ttlcache': 1.4.1 '@react-native/debugger-frontend': 0.79.2 @@ -10641,7 +10796,7 @@ snapshots: nullthrows: 1.1.1 open: 7.4.2 serve-static: 1.16.2 - ws: 6.2.3 + ws: 6.2.3(bufferutil@4.0.8)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -10653,34 +10808,38 @@ snapshots: '@react-native/normalize-colors@0.79.2': {} - '@react-native/virtualized-lists@0.79.2(@types/react@18.3.23)(react-native@0.79.2)(react@18.3.1)': + '@react-native/virtualized-lists@0.79.2(@types/react@18.3.23)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)': dependencies: - '@types/react': 18.3.23 invariant: 2.2.4 nullthrows: 1.1.1 react: 18.3.1 - react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1) + react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) + optionalDependencies: + '@types/react': 18.3.23 '@rollup/plugin-terser@0.4.4(rollup@3.29.5)': dependencies: - rollup: 3.29.5 serialize-javascript: 6.0.2 smob: 1.5.0 terser: 5.40.0 + optionalDependencies: + rollup: 3.29.5 '@rollup/plugin-typescript@11.1.6(rollup@3.29.5)(tslib@2.8.1)(typescript@5.6.3)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@3.29.5) resolve: 1.22.10 + typescript: 5.6.3 + optionalDependencies: rollup: 3.29.5 tslib: 2.8.1 - typescript: 5.6.3 '@rollup/pluginutils@5.1.4(rollup@3.29.5)': dependencies: '@types/estree': 1.0.7 estree-walker: 2.0.2 picomatch: 4.0.2 + optionalDependencies: rollup: 3.29.5 '@rollup/rollup-android-arm-eabi@4.41.1': @@ -11035,6 +11194,8 @@ snapshots: '@tootallnate/once@1.1.2': optional: true + '@tootallnate/once@2.0.0': {} + '@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3)': dependencies: '@babel/generator': 7.27.3 @@ -11225,7 +11386,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.3) @@ -11240,11 +11401,12 @@ snapshots: natural-compare: 1.4.0 semver: 7.7.2 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0)(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) @@ -11257,6 +11419,7 @@ snapshots: ignore: 5.3.2 natural-compare: 1.4.0 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11277,6 +11440,7 @@ snapshots: '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.1 eslint: 8.57.1 + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11289,6 +11453,7 @@ snapshots: '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.1 eslint: 8.57.1 + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11328,6 +11493,7 @@ snapshots: debug: 4.4.1 eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11339,6 +11505,7 @@ snapshots: debug: 4.4.1 eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11358,6 +11525,7 @@ snapshots: is-glob: 4.0.3 semver: 7.7.2 tsutils: 3.21.0(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11372,6 +11540,7 @@ snapshots: minimatch: 9.0.3 semver: 7.7.2 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11386,6 +11555,7 @@ snapshots: minimatch: 9.0.5 semver: 7.7.2 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11495,12 +11665,29 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.4(vite@5.4.19)': + '@vitest/mocker@3.1.4(vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0))': dependencies: '@vitest/spy': 3.1.4 estree-walker: 3.0.3 magic-string: 0.30.17 - vite: 5.4.19(@types/node@18.19.108) + optionalDependencies: + vite: 5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) + + '@vitest/mocker@3.1.4(vite@5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0))': + dependencies: + '@vitest/spy': 3.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0) + + '@vitest/mocker@3.1.4(vite@5.4.19(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0))': + dependencies: + '@vitest/spy': 3.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0) '@vitest/pretty-format@3.1.4': dependencies: @@ -11530,7 +11717,7 @@ snapshots: pathe: 1.1.2 picocolors: 1.1.1 sirv: 2.0.4 - vitest: 3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1) + vitest: 3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1)(lightningcss@1.27.0)(terser@5.40.0) '@vitest/utils@1.6.1': dependencies: @@ -11587,7 +11774,6 @@ snapshots: debug: 4.4.1 transitivePeerDependencies: - supports-color - optional: true agent-base@7.1.3: {} @@ -11740,6 +11926,8 @@ snapshots: arrgv@1.0.2: {} + arrify@2.0.1: {} + arrify@3.0.0: {} asap@2.0.6: {} @@ -11973,6 +12161,10 @@ snapshots: big-integer@1.6.52: {} + big.js@6.2.2: {} + + bignumber.js@9.3.1: {} + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -12039,6 +12231,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@4.9.2: @@ -12684,9 +12878,27 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.27.2(bun-types@1.2.15): - dependencies: + drizzle-orm@0.27.2(@aws-sdk/client-rds-data@3.817.0)(@cloudflare/workers-types@4.20250529.0)(@libsql/client@0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.2)(@types/sql.js@1.4.9)(@vercel/postgres@0.8.0)(better-sqlite3@11.10.0)(bun-types@1.2.15)(knex@2.5.1(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.0)(sqlite3@5.1.7))(kysely@0.25.0)(mysql2@3.14.1)(pg@8.16.0)(postgres@3.4.7)(sql.js@1.13.0)(sqlite3@5.1.7): + optionalDependencies: + '@aws-sdk/client-rds-data': 3.817.0 + '@cloudflare/workers-types': 4.20250529.0 + '@libsql/client': 0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) + '@neondatabase/serverless': 0.10.0 + '@opentelemetry/api': 1.9.0 + '@planetscale/database': 1.19.0 + '@types/better-sqlite3': 7.6.13 + '@types/pg': 8.15.2 + '@types/sql.js': 1.4.9 + '@vercel/postgres': 0.8.0 + better-sqlite3: 11.10.0 bun-types: 1.2.15 + knex: 2.5.1(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.0)(sqlite3@5.1.7) + kysely: 0.25.0 + mysql2: 3.14.1 + pg: 8.16.0 + postgres: 3.4.7 + sql.js: 1.13.0 + sqlite3: 5.1.7 drizzle-prisma-generator@0.1.7: dependencies: @@ -12700,8 +12912,19 @@ snapshots: duplexer@0.1.2: {} + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.161: {} @@ -13095,19 +13318,19 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.3) debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.3) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 @@ -13116,7 +13339,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13127,6 +13350,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -13134,13 +13359,14 @@ snapshots: eslint-plugin-no-instanceof@1.0.1: {} - eslint-plugin-prettier@5.4.1(eslint-config-prettier@9.1.0)(eslint@8.57.1)(prettier@3.5.3): + eslint-plugin-prettier@5.4.1(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.5.3): dependencies: eslint: 8.57.1 - eslint-config-prettier: 9.1.0(eslint@8.57.1) prettier: 3.5.3 prettier-linter-helpers: 1.0.0 synckit: 0.11.8 + optionalDependencies: + eslint-config-prettier: 9.1.0(eslint@8.57.1) eslint-plugin-unicorn@48.0.1(eslint@8.57.1): dependencies: @@ -13161,11 +13387,12 @@ snapshots: semver: 7.7.2 strip-indent: 3.0.0 - eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.1): + eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-rule-composer: 0.3.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) eslint-rule-composer@0.3.0: {} @@ -13318,39 +13545,39 @@ snapshots: expect-type@1.2.1: {} - expo-asset@11.1.5(expo@53.0.9)(react-native@0.79.2)(react@18.3.1): + expo-asset@11.1.5(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: '@expo/image-utils': 0.7.4 - expo: 53.0.9(@babel/core@7.27.3)(react-native@0.79.2)(react@18.3.1) - expo-constants: 17.1.6(expo@53.0.9)(react-native@0.79.2) + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo-constants: 17.1.6(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) react: 18.3.1 - react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1) + react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - supports-color - expo-constants@17.1.6(expo@53.0.9)(react-native@0.79.2): + expo-constants@17.1.6(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: '@expo/config': 11.0.10 '@expo/env': 1.0.5 - expo: 53.0.9(@babel/core@7.27.3)(react-native@0.79.2)(react@18.3.1) - react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1) + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - supports-color - expo-file-system@18.1.10(expo@53.0.9)(react-native@0.79.2): + expo-file-system@18.1.10(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: - expo: 53.0.9(@babel/core@7.27.3)(react-native@0.79.2)(react@18.3.1) - react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1) + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) - expo-font@13.3.1(expo@53.0.9)(react@18.3.1): + expo-font@13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: - expo: 53.0.9(@babel/core@7.27.3)(react-native@0.79.2)(react@18.3.1) + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) fontfaceobserver: 2.3.0 react: 18.3.1 - expo-keep-awake@14.1.4(expo@53.0.9)(react@18.3.1): + expo-keep-awake@14.1.4(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: - expo: 53.0.9(@babel/core@7.27.3)(react-native@0.79.2)(react@18.3.1) + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) react: 18.3.1 expo-modules-autolinking@2.1.10: @@ -13367,31 +13594,31 @@ snapshots: dependencies: invariant: 2.2.4 - expo-sqlite@14.0.6(expo@53.0.9): + expo-sqlite@14.0.6(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: '@expo/websql': 1.0.1 - expo: 53.0.9(@babel/core@7.27.3)(react-native@0.79.2)(react@18.3.1) + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) - expo@53.0.9(@babel/core@7.27.3)(react-native@0.79.2)(react@18.3.1): + expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3): dependencies: '@babel/runtime': 7.27.3 - '@expo/cli': 0.24.13 + '@expo/cli': 0.24.13(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@expo/config': 11.0.10 '@expo/config-plugins': 10.0.2 '@expo/fingerprint': 0.12.4 '@expo/metro-config': 0.20.14 - '@expo/vector-icons': 14.1.0(expo-font@13.3.1)(react-native@0.79.2)(react@18.3.1) + '@expo/vector-icons': 14.1.0(expo-font@13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) babel-preset-expo: 13.1.11(@babel/core@7.27.3) - expo-asset: 11.1.5(expo@53.0.9)(react-native@0.79.2)(react@18.3.1) - expo-constants: 17.1.6(expo@53.0.9)(react-native@0.79.2) - expo-file-system: 18.1.10(expo@53.0.9)(react-native@0.79.2) - expo-font: 13.3.1(expo@53.0.9)(react@18.3.1) - expo-keep-awake: 14.1.4(expo@53.0.9)(react@18.3.1) + expo-asset: 11.1.5(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + expo-constants: 17.1.6(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) + expo-file-system: 18.1.10(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) + expo-font: 13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + expo-keep-awake: 14.1.4(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.8)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) expo-modules-autolinking: 2.1.10 expo-modules-core: 2.3.13 react: 18.3.1 - react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1) - react-native-edge-to-edge: 1.6.0(react-native@0.79.2)(react@18.3.1) + react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) + react-native-edge-to-edge: 1.6.0(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) whatwg-url-without-unicode: 8.0.0-3 transitivePeerDependencies: - '@babel/core' @@ -13443,6 +13670,8 @@ snapshots: dependencies: type: 2.7.3 + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -13480,7 +13709,7 @@ snapshots: bser: 2.1.1 fdir@6.4.5(picomatch@4.0.2): - dependencies: + optionalDependencies: picomatch: 4.0.2 fetch-blob@3.2.0: @@ -13628,6 +13857,23 @@ snapshots: wide-align: 1.1.5 optional: true + gaxios@7.1.3: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.3 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + gel@2.1.0: dependencies: '@petamoriken/float16': 3.9.2 @@ -13775,12 +14021,33 @@ snapshots: globrex@0.1.2: {} + google-auth-library@10.5.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.3 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + gtoken: 8.0.0 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} graphemer@1.4.0: {} + gtoken@8.0.0: + dependencies: + gaxios: 7.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + hanji@0.0.5: dependencies: lodash.throttle: 4.1.1 @@ -13841,6 +14108,8 @@ snapshots: dependencies: lru-cache: 10.4.3 + html-entities@2.6.0: {} + http-cache-semantics@4.2.0: optional: true @@ -13861,13 +14130,20 @@ snapshots: - supports-color optional: true + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 debug: 4.4.1 transitivePeerDependencies: - supports-color - optional: true https-proxy-agent@7.0.6: dependencies: @@ -14257,6 +14533,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-diff@0.9.0: @@ -14313,6 +14593,17 @@ snapshots: junk@4.0.1: {} + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -14327,7 +14618,6 @@ snapshots: knex@2.5.1(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.0)(sqlite3@5.1.7): dependencies: - better-sqlite3: 11.10.0 colorette: 2.0.19 commander: 10.0.1 debug: 4.3.4 @@ -14337,14 +14627,16 @@ snapshots: getopts: 2.3.0 interpret: 2.2.0 lodash: 4.17.21 - mysql2: 3.14.1 - pg: 8.16.0 pg-connection-string: 2.6.1 rechoir: 0.8.0 resolve-from: 5.0.0 - sqlite3: 5.1.7 tarn: 3.0.2 tildify: 2.0.0 + optionalDependencies: + better-sqlite3: 11.10.0 + mysql2: 3.14.1 + pg: 8.16.0 + sqlite3: 5.1.7 transitivePeerDependencies: - supports-color @@ -14618,13 +14910,13 @@ snapshots: transitivePeerDependencies: - supports-color - metro-config@0.82.4: + metro-config@0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3): dependencies: connect: 3.7.0 cosmiconfig: 5.2.1 flow-enums-runtime: 0.0.6 jest-validate: 29.7.0 - metro: 0.82.4 + metro: 0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3) metro-cache: 0.82.4 metro-core: 0.82.4 metro-runtime: 0.82.4 @@ -14704,14 +14996,14 @@ snapshots: transitivePeerDependencies: - supports-color - metro-transform-worker@0.82.4: + metro-transform-worker@0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3): dependencies: '@babel/core': 7.27.3 '@babel/generator': 7.27.3 '@babel/parser': 7.27.3 '@babel/types': 7.27.3 flow-enums-runtime: 0.0.6 - metro: 0.82.4 + metro: 0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3) metro-babel-transformer: 0.82.4 metro-cache: 0.82.4 metro-cache-key: 0.82.4 @@ -14724,7 +15016,7 @@ snapshots: - supports-color - utf-8-validate - metro@0.82.4: + metro@0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3): dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.27.3 @@ -14750,7 +15042,7 @@ snapshots: metro-babel-transformer: 0.82.4 metro-cache: 0.82.4 metro-cache-key: 0.82.4 - metro-config: 0.82.4 + metro-config: 0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3) metro-core: 0.82.4 metro-file-map: 0.82.4 metro-resolver: 0.82.4 @@ -14758,13 +15050,13 @@ snapshots: metro-source-map: 0.82.4 metro-symbolicate: 0.82.4 metro-transform-plugins: 0.82.4 - metro-transform-worker: 0.82.4 + metro-transform-worker: 0.82.4(bufferutil@4.0.8)(utf-8-validate@6.0.3) mime-types: 2.1.35 nullthrows: 1.1.1 serialize-error: 2.1.0 source-map: 0.5.7 throat: 5.0.0 - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.3) yargs: 17.7.2 transitivePeerDependencies: - bufferutil @@ -15382,15 +15674,21 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(tsx@3.14.0): + postcss-load-config@6.0.1(postcss@8.5.4)(tsx@3.14.0)(yaml@2.8.0): dependencies: lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.4 tsx: 3.14.0 + yaml: 2.8.0 - postcss-load-config@6.0.1(tsx@4.19.4): + postcss-load-config@6.0.1(postcss@8.5.4)(tsx@4.19.4)(yaml@2.8.0): dependencies: lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.4 tsx: 4.19.4 + yaml: 2.8.0 postcss@8.4.49: dependencies: @@ -15562,32 +15860,31 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-devtools-core@6.1.2: + react-devtools-core@6.1.2(bufferutil@4.0.8)(utf-8-validate@6.0.3): dependencies: shell-quote: 1.8.2 - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - utf-8-validate react-is@18.3.1: {} - react-native-edge-to-edge@1.6.0(react-native@0.79.2)(react@18.3.1): + react-native-edge-to-edge@1.6.0(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: react: 18.3.1 - react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1) + react-native: 0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) - react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(react@18.3.1): + react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.79.2 '@react-native/codegen': 0.79.2(@babel/core@7.27.3) - '@react-native/community-cli-plugin': 0.79.2 + '@react-native/community-cli-plugin': 0.79.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@react-native/gradle-plugin': 0.79.2 '@react-native/js-polyfills': 0.79.2 '@react-native/normalize-colors': 0.79.2 - '@react-native/virtualized-lists': 0.79.2(@types/react@18.3.23)(react-native@0.79.2)(react@18.3.1) - '@types/react': 18.3.23 + '@react-native/virtualized-lists': 0.79.2(@types/react@18.3.23)(react-native@0.79.2(@babel/core@7.27.3)(@types/react@18.3.23)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -15608,15 +15905,17 @@ snapshots: pretty-format: 29.7.0 promise: 8.3.0 react: 18.3.1 - react-devtools-core: 6.1.2 + react-devtools-core: 6.1.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) react-refresh: 0.14.2 regenerator-runtime: 0.13.11 scheduler: 0.25.0 semver: 7.7.2 stacktrace-parser: 0.1.11 whatwg-fetch: 3.6.20 - ws: 6.2.3 + ws: 6.2.3(bufferutil@4.0.8)(utf-8-validate@6.0.3) yargs: 17.7.2 + optionalDependencies: + '@types/react': 18.3.23 transitivePeerDependencies: - '@babel/core' - '@react-native-community/cli' @@ -15768,6 +16067,13 @@ snapshots: onetime: 2.0.1 signal-exit: 3.0.7 + retry-request@8.0.2: + dependencies: + extend: 3.0.2 + teeny-request: 10.1.0 + transitivePeerDependencies: + - supports-color + retry@0.12.0: optional: true @@ -16231,6 +16537,12 @@ snapshots: dependencies: duplexer: 0.1.2 + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + + stream-shift@1.0.3: {} + streamsearch@1.1.0: {} string-width@4.2.3: @@ -16300,6 +16612,8 @@ snapshots: structured-headers@0.4.1: {} + stubs@3.0.0: {} + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -16384,6 +16698,15 @@ snapshots: tarn@3.0.2: {} + teeny-request@10.1.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 3.3.2 + stream-events: 1.0.5 + transitivePeerDependencies: + - supports-color + temp-dir@2.0.0: {} temp-dir@3.0.0: {} @@ -16502,7 +16825,7 @@ snapshots: yn: 3.1.1 tsconfck@3.1.6(typescript@5.6.3): - dependencies: + optionalDependencies: typescript: 5.6.3 tsconfig-paths@3.15.0: @@ -16516,7 +16839,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(tsx@3.14.0)(typescript@5.6.3): + tsup@8.5.0(postcss@8.5.4)(tsx@3.14.0)(typescript@5.6.3)(yaml@2.8.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.5) cac: 6.7.14 @@ -16527,7 +16850,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(tsx@3.14.0) + postcss-load-config: 6.0.1(postcss@8.5.4)(tsx@3.14.0)(yaml@2.8.0) resolve-from: 5.0.0 rollup: 4.41.1 source-map: 0.8.0-beta.0 @@ -16535,6 +16858,8 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.14 tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.4 typescript: 5.6.3 transitivePeerDependencies: - jiti @@ -16542,7 +16867,7 @@ snapshots: - tsx - yaml - tsup@8.5.0(tsx@4.19.4)(typescript@5.6.3): + tsup@8.5.0(postcss@8.5.4)(tsx@4.19.4)(typescript@5.6.3)(yaml@2.8.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.5) cac: 6.7.14 @@ -16553,7 +16878,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(tsx@4.19.4) + postcss-load-config: 6.0.1(postcss@8.5.4)(tsx@4.19.4)(yaml@2.8.0) resolve-from: 5.0.0 rollup: 4.41.1 source-map: 0.8.0-beta.0 @@ -16561,6 +16886,8 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.14 tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.4 typescript: 5.6.3 transitivePeerDependencies: - jiti @@ -16791,7 +17118,7 @@ snapshots: v8-compile-cache-lib@3.0.1: {} valibot@1.0.0-beta.7(typescript@5.6.3): - dependencies: + optionalDependencies: typescript: 5.6.3 validate-npm-package-license@3.0.4: @@ -16807,13 +17134,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.1.4(@types/node@18.19.108): + vite-node@3.1.4(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.19(@types/node@18.19.108) + vite: 5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) transitivePeerDependencies: - '@types/node' - less @@ -16825,13 +17152,13 @@ snapshots: - supports-color - terser - vite-node@3.1.4(@types/node@20.17.55): + vite-node@3.1.4(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.19(@types/node@20.17.55) + vite: 5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0) transitivePeerDependencies: - '@types/node' - less @@ -16843,13 +17170,13 @@ snapshots: - supports-color - terser - vite-node@3.1.4(@types/node@22.15.27): + vite-node@3.1.4(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.19(@types/node@22.15.27) + vite: 5.4.19(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0) transitivePeerDependencies: - '@types/node' - less @@ -16861,47 +17188,65 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@4.3.2(typescript@5.6.3): + vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.6.3) + optionalDependencies: + vite: 5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.19(@types/node@18.19.108): + vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0)): + dependencies: + debug: 4.4.1 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.6.3) + optionalDependencies: + vite: 5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0) + transitivePeerDependencies: + - supports-color + - typescript + + vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0): dependencies: - '@types/node': 18.19.108 esbuild: 0.21.5 postcss: 8.5.4 rollup: 4.41.1 optionalDependencies: + '@types/node': 18.19.108 fsevents: 2.3.3 + lightningcss: 1.27.0 + terser: 5.40.0 - vite@5.4.19(@types/node@20.17.55): + vite@5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0): dependencies: - '@types/node': 20.17.55 esbuild: 0.21.5 postcss: 8.5.4 rollup: 4.41.1 optionalDependencies: + '@types/node': 20.17.55 fsevents: 2.3.3 + lightningcss: 1.27.0 + terser: 5.40.0 - vite@5.4.19(@types/node@22.15.27): + vite@5.4.19(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0): dependencies: - '@types/node': 22.15.27 esbuild: 0.21.5 postcss: 8.5.4 rollup: 4.41.1 optionalDependencies: + '@types/node': 22.15.27 fsevents: 2.3.3 + lightningcss: 1.27.0 + terser: 5.40.0 - vitest@3.1.4(@types/node@18.19.108): + vitest@3.1.4(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0): dependencies: - '@types/node': 18.19.108 '@vitest/expect': 3.1.4 - '@vitest/mocker': 3.1.4(vite@5.4.19) + '@vitest/mocker': 3.1.4(vite@5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0)) '@vitest/pretty-format': 3.1.4 '@vitest/runner': 3.1.4 '@vitest/snapshot': 3.1.4 @@ -16918,9 +17263,11 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.19(@types/node@18.19.108) - vite-node: 3.1.4(@types/node@18.19.108) + vite: 5.4.19(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) + vite-node: 3.1.4(@types/node@18.19.108)(lightningcss@1.27.0)(terser@5.40.0) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 18.19.108 transitivePeerDependencies: - less - lightningcss @@ -16932,16 +17279,14 @@ snapshots: - supports-color - terser - vitest@3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1): + vitest@3.1.4(@types/node@20.17.55)(@vitest/ui@1.6.1)(lightningcss@1.27.0)(terser@5.40.0): dependencies: - '@types/node': 20.17.55 '@vitest/expect': 3.1.4 - '@vitest/mocker': 3.1.4(vite@5.4.19) + '@vitest/mocker': 3.1.4(vite@5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0)) '@vitest/pretty-format': 3.1.4 '@vitest/runner': 3.1.4 '@vitest/snapshot': 3.1.4 '@vitest/spy': 3.1.4 - '@vitest/ui': 1.6.1(vitest@3.1.4) '@vitest/utils': 3.1.4 chai: 5.2.0 debug: 4.4.1 @@ -16954,9 +17299,12 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.19(@types/node@20.17.55) - vite-node: 3.1.4(@types/node@20.17.55) + vite: 5.4.19(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0) + vite-node: 3.1.4(@types/node@20.17.55)(lightningcss@1.27.0)(terser@5.40.0) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.17.55 + '@vitest/ui': 1.6.1(vitest@3.1.4) transitivePeerDependencies: - less - lightningcss @@ -16968,11 +17316,10 @@ snapshots: - supports-color - terser - vitest@3.1.4(@types/node@22.15.27): + vitest@3.1.4(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0): dependencies: - '@types/node': 22.15.27 '@vitest/expect': 3.1.4 - '@vitest/mocker': 3.1.4(vite@5.4.19) + '@vitest/mocker': 3.1.4(vite@5.4.19(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0)) '@vitest/pretty-format': 3.1.4 '@vitest/runner': 3.1.4 '@vitest/snapshot': 3.1.4 @@ -16989,9 +17336,11 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.19(@types/node@22.15.27) - vite-node: 3.1.4(@types/node@22.15.27) + vite: 5.4.19(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0) + vite-node: 3.1.4(@types/node@22.15.27)(lightningcss@1.27.0)(terser@5.40.0) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.15.27 transitivePeerDependencies: - less - lightningcss @@ -17130,18 +17479,27 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@6.2.3: + ws@6.2.3(bufferutil@4.0.8)(utf-8-validate@6.0.3): dependencies: async-limiter: 1.0.1 + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 6.0.3 - ws@7.5.10: {} + ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.3): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 6.0.3 ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3): - dependencies: + optionalDependencies: bufferutil: 4.0.8 utf-8-validate: 6.0.3 - ws@8.18.2: {} + ws@8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.3): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 6.0.3 xcode@3.0.1: dependencies: From 8763327f526b58ec69643ec651bb4eea05ce5c6d Mon Sep 17 00:00:00 2001 From: BridgerB Date: Sun, 7 Dec 2025 14:36:07 -0700 Subject: [PATCH 2/2] Add GEOGRAPHY, INTERVAL, and ARRAY column types for BigQuery - Add geography.ts for GEOGRAPHY type (geospatial data as WKT/GeoJSON string) - Add interval.ts for INTERVAL type (duration in canonical format) - Add .array() method to BigQueryColumnBuilder for ARRAY support - Export new column types from index.ts and all.ts --- drizzle-orm/src/bigquery-core/columns/all.ts | 4 + .../src/bigquery-core/columns/common.ts | 120 +++++++++++++++++- .../src/bigquery-core/columns/geography.ts | 54 ++++++++ .../src/bigquery-core/columns/index.ts | 2 + .../src/bigquery-core/columns/interval.ts | 55 ++++++++ 5 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 drizzle-orm/src/bigquery-core/columns/geography.ts create mode 100644 drizzle-orm/src/bigquery-core/columns/interval.ts diff --git a/drizzle-orm/src/bigquery-core/columns/all.ts b/drizzle-orm/src/bigquery-core/columns/all.ts index be58d129a3..c8053337c1 100644 --- a/drizzle-orm/src/bigquery-core/columns/all.ts +++ b/drizzle-orm/src/bigquery-core/columns/all.ts @@ -3,7 +3,9 @@ import { bytes } from './bytes.ts'; import { date } from './date.ts'; import { datetime } from './datetime.ts'; import { float64 } from './float64.ts'; +import { geography } from './geography.ts'; import { int64 } from './int64.ts'; +import { interval } from './interval.ts'; import { json } from './json.ts'; import { bignumeric, numeric } from './numeric.ts'; import { string } from './string.ts'; @@ -17,7 +19,9 @@ export function getBigQueryColumnBuilders() { date, datetime, float64, + geography, int64, + interval, json, numeric, bignumeric, diff --git a/drizzle-orm/src/bigquery-core/columns/common.ts b/drizzle-orm/src/bigquery-core/columns/common.ts index d1180fb82e..9fc7913894 100644 --- a/drizzle-orm/src/bigquery-core/columns/common.ts +++ b/drizzle-orm/src/bigquery-core/columns/common.ts @@ -11,9 +11,9 @@ import type { } from '~/column-builder.ts'; import type { ColumnBaseConfig } from '~/column.ts'; import { Column } from '~/column.ts'; -import { entityKind } from '~/entity.ts'; +import { entityKind, is } from '~/entity.ts'; import type { SQL } from '~/sql/sql.ts'; -import type { Update } from '~/utils.ts'; +import type { Simplify, Update } from '~/utils.ts'; import { uniqueKeyName } from '../unique-constraint.ts'; export interface BigQueryColumnBuilderBase< @@ -44,6 +44,34 @@ export abstract class BigQueryColumnBuilder< return this; } + /** + * Creates an ARRAY column type wrapping this column's type. + * + * @example + * ```ts + * const table = bigqueryTable('test', { + * tags: string('tags').array(), + * scores: int64('scores').array(), + * }); + * ``` + */ + array(): BigQueryArrayBuilder< + & { + name: T['name']; + dataType: 'array'; + columnType: 'BigQueryArray'; + data: T['data'][]; + driverParam: T['driverParam'][]; + enumValues: T['enumValues']; + baseBuilder: T; + } + & (T extends { notNull: true } ? { notNull: true } : {}) + & (T extends { hasDefault: true } ? { hasDefault: true } : {}), + T + > { + return new BigQueryArrayBuilder(this.config.name, this as BigQueryColumnBuilder); + } + generatedAlwaysAs(as: SQL | T['data'] | (() => SQL), config?: BigQueryGeneratedColumnConfig): HasGenerated { @@ -83,3 +111,91 @@ export abstract class BigQueryColumn< export type AnyBigQueryColumn> = {}> = BigQueryColumn< Required, TPartial>> >; + +// ARRAY type support +export type BigQueryArrayColumnBuilderBaseConfig = ColumnBuilderBaseConfig<'array', 'BigQueryArray'> & { + baseBuilder: ColumnBuilderBaseConfig; +}; + +export class BigQueryArrayBuilder< + T extends BigQueryArrayColumnBuilderBaseConfig, + TBase extends ColumnBuilderBaseConfig | BigQueryArrayColumnBuilderBaseConfig, +> extends BigQueryColumnBuilder< + T, + { + baseBuilder: TBase extends BigQueryArrayColumnBuilderBaseConfig ? BigQueryArrayBuilder< + TBase, + TBase extends { baseBuilder: infer TBaseBuilder extends ColumnBuilderBaseConfig } ? TBaseBuilder + : never + > + : BigQueryColumnBuilder>>>; + }, + { + baseBuilder: TBase extends BigQueryArrayColumnBuilderBaseConfig ? BigQueryArrayBuilder< + TBase, + TBase extends { baseBuilder: infer TBaseBuilder extends ColumnBuilderBaseConfig } ? TBaseBuilder + : never + > + : BigQueryColumnBuilder>>>; + } +> { + static override readonly [entityKind] = 'BigQueryArrayBuilder'; + + constructor( + name: string, + baseBuilder: BigQueryArrayBuilder['config']['baseBuilder'], + ) { + super(name, 'array', 'BigQueryArray'); + this.config.baseBuilder = baseBuilder; + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryArray & { baseBuilder: T['baseBuilder'] }, TBase> { + const baseColumn = this.config.baseBuilder.build(table); + return new BigQueryArray & { baseBuilder: T['baseBuilder'] }, TBase>( + table as AnyBigQueryTable<{ name: MakeColumnConfig['tableName'] }>, + this.config as ColumnBuilderRuntimeConfig, + baseColumn, + ); + } +} + +export class BigQueryArray< + T extends ColumnBaseConfig<'array', 'BigQueryArray'> & { + baseBuilder: ColumnBuilderBaseConfig; + }, + TBase extends ColumnBuilderBaseConfig, +> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryArray'; + + constructor( + table: AnyBigQueryTable<{ name: T['tableName'] }>, + config: BigQueryArrayBuilder['config'], + readonly baseColumn: BigQueryColumn, + ) { + super(table as BigQueryTable, config); + } + + getSQLType(): string { + return `ARRAY<${this.baseColumn.getSQLType()}>`; + } + + // BigQuery returns arrays as native JavaScript arrays, so no special parsing needed + // Unlike PostgreSQL which returns arrays as strings like "{val1,val2}" + override mapFromDriverValue(value: unknown[]): T['data'] { + return value.map((v) => this.baseColumn.mapFromDriverValue(v)); + } + + override mapToDriverValue(value: unknown[], isNestedArray = false): unknown[] { + const a = value.map((v) => + v === null + ? null + : is(this.baseColumn, BigQueryArray) + ? (this.baseColumn as unknown as BigQueryArray).mapToDriverValue(v as unknown[], true) + : this.baseColumn.mapToDriverValue(v) + ); + return a; + } +} diff --git a/drizzle-orm/src/bigquery-core/columns/geography.ts b/drizzle-orm/src/bigquery-core/columns/geography.ts new file mode 100644 index 0000000000..f2cac12e70 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/geography.ts @@ -0,0 +1,54 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +// GEOGRAPHY - Geospatial data in WKT or GeoJSON format +export type BigQueryGeographyBuilderInitial = BigQueryGeographyBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryGeography'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryGeographyBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryGeographyBuilder'; + + constructor(name: T['name']) { + super(name, 'string', 'BigQueryGeography'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryGeography> { + return new BigQueryGeography>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryGeography> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryGeography'; + + getSQLType(): string { + return 'GEOGRAPHY'; + } + + // BigQuery returns GEOGRAPHY as GeoJSON object or WKT string + // We keep it as string for simplicity - user can parse if needed +} + +export function geography(): BigQueryGeographyBuilderInitial<''>; +export function geography(name: TName): BigQueryGeographyBuilderInitial; +export function geography(a?: string) { + const { name } = getColumnNameAndConfig(a, undefined); + return new BigQueryGeographyBuilder(name); +} diff --git a/drizzle-orm/src/bigquery-core/columns/index.ts b/drizzle-orm/src/bigquery-core/columns/index.ts index fbe0554456..3a248f1a0f 100644 --- a/drizzle-orm/src/bigquery-core/columns/index.ts +++ b/drizzle-orm/src/bigquery-core/columns/index.ts @@ -5,7 +5,9 @@ export * from './common.ts'; export * from './date.ts'; export * from './datetime.ts'; export * from './float64.ts'; +export * from './geography.ts'; export * from './int64.ts'; +export * from './interval.ts'; export * from './json.ts'; export * from './numeric.ts'; export * from './string.ts'; diff --git a/drizzle-orm/src/bigquery-core/columns/interval.ts b/drizzle-orm/src/bigquery-core/columns/interval.ts new file mode 100644 index 0000000000..f6c90dcde8 --- /dev/null +++ b/drizzle-orm/src/bigquery-core/columns/interval.ts @@ -0,0 +1,55 @@ +import type { AnyBigQueryTable } from '~/bigquery-core/table.ts'; +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import { getColumnNameAndConfig } from '~/utils.ts'; +import { BigQueryColumn, BigQueryColumnBuilder } from './common.ts'; + +// INTERVAL - Duration type in BigQuery +// Canonical format: "Y-M D H:M:S" (year-month day hour:minute:second) +export type BigQueryIntervalBuilderInitial = BigQueryIntervalBuilder<{ + name: TName; + dataType: 'string'; + columnType: 'BigQueryInterval'; + data: string; + driverParam: string; + enumValues: undefined; +}>; + +export class BigQueryIntervalBuilder> + extends BigQueryColumnBuilder +{ + static override readonly [entityKind]: string = 'BigQueryIntervalBuilder'; + + constructor(name: T['name']) { + super(name, 'string', 'BigQueryInterval'); + } + + /** @internal */ + override build( + table: AnyBigQueryTable<{ name: TTableName }>, + ): BigQueryInterval> { + return new BigQueryInterval>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class BigQueryInterval> extends BigQueryColumn { + static override readonly [entityKind]: string = 'BigQueryInterval'; + + getSQLType(): string { + return 'INTERVAL'; + } + + // BigQuery INTERVAL is returned in canonical format: "Y-M D H:M:S" + // Keep as string, user can parse if needed +} + +export function interval(): BigQueryIntervalBuilderInitial<''>; +export function interval(name: TName): BigQueryIntervalBuilderInitial; +export function interval(a?: string) { + const { name } = getColumnNameAndConfig(a, undefined); + return new BigQueryIntervalBuilder(name); +}