From 2881b4ce15ea72865c9d3c7e87df37237b2b62e1 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Fri, 13 Oct 2023 01:16:23 +0300 Subject: [PATCH] Resolve circular dependencies --- drizzle-orm/src/alias.ts | 3 +- drizzle-orm/src/index.ts | 1 + drizzle-orm/src/mysql-core/dialect.ts | 4 +- drizzle-orm/src/mysql-core/index.ts | 1 + .../src/mysql-core/query-builders/query.ts | 16 +- .../src/mysql-core/query-builders/select.ts | 3 +- drizzle-orm/src/mysql-core/utils.ts | 4 +- drizzle-orm/src/mysql-core/view-common.ts | 1 + drizzle-orm/src/mysql-core/view.ts | 3 +- drizzle-orm/src/pg-core/columns/array.ts | 2 +- drizzle-orm/src/pg-core/dialect.ts | 3 +- drizzle-orm/src/pg-core/index.ts | 2 + .../src/pg-core/query-builders/select.ts | 3 +- drizzle-orm/src/pg-core/utils.ts | 101 +-- drizzle-orm/src/pg-core/utils/array.ts | 95 +++ drizzle-orm/src/pg-core/utils/index.ts | 1 + drizzle-orm/src/pg-core/view-common.ts | 1 + drizzle-orm/src/pg-core/view.ts | 3 +- drizzle-orm/src/sql/expressions/conditions.ts | 2 +- drizzle-orm/src/sql/expressions/select.ts | 4 +- drizzle-orm/src/sql/index.ts | 585 +----------------- drizzle-orm/src/sql/sql.ts | 584 +++++++++++++++++ drizzle-orm/src/sqlite-core/dialect.ts | 2 +- .../src/sqlite-core/query-builders/select.ts | 3 +- .../src/sqlite-core/unique-constraint.ts | 2 +- drizzle-orm/src/sqlite-core/utils.ts | 5 +- drizzle-orm/src/sqlite-core/view-common.ts | 1 + drizzle-orm/src/sqlite-core/view.ts | 5 +- drizzle-orm/src/sqlite-proxy/session.ts | 4 +- drizzle-orm/src/subquery.ts | 3 +- drizzle-orm/src/table.ts | 4 +- drizzle-orm/src/utils.ts | 3 +- drizzle-orm/src/view-common.ts | 1 + drizzle-orm/src/view.ts | 3 +- drizzle-typebox/tests/utils.ts | 3 +- drizzle-valibot/tests/utils.ts | 3 +- drizzle-zod/tests/utils.ts | 2 +- 37 files changed, 736 insertions(+), 730 deletions(-) create mode 100644 drizzle-orm/src/mysql-core/view-common.ts create mode 100644 drizzle-orm/src/pg-core/utils/array.ts create mode 100644 drizzle-orm/src/pg-core/utils/index.ts create mode 100644 drizzle-orm/src/pg-core/view-common.ts create mode 100644 drizzle-orm/src/sql/sql.ts create mode 100644 drizzle-orm/src/sqlite-core/view-common.ts create mode 100644 drizzle-orm/src/view-common.ts diff --git a/drizzle-orm/src/alias.ts b/drizzle-orm/src/alias.ts index e570921d2..06269921e 100644 --- a/drizzle-orm/src/alias.ts +++ b/drizzle-orm/src/alias.ts @@ -4,7 +4,8 @@ import { entityKind, is } from './entity.ts'; import type { Relation } from './relations.ts'; import { SQL, sql } from './sql/index.ts'; import { Table } from './table.ts'; -import { type View, ViewBaseConfig } from './view.ts'; +import { ViewBaseConfig } from './view-common.ts'; +import type { View } from './view.ts'; export class ColumnAliasProxyHandler implements ProxyHandler { static readonly [entityKind]: string = 'ColumnAliasProxyHandler'; diff --git a/drizzle-orm/src/index.ts b/drizzle-orm/src/index.ts index 2eb9a58ca..66b45c585 100644 --- a/drizzle-orm/src/index.ts +++ b/drizzle-orm/src/index.ts @@ -12,4 +12,5 @@ export * from './sql/index.ts'; export * from './subquery.ts'; export * from './table.ts'; export * from './utils.ts'; +export * from './view-common.ts'; export * from './view.ts'; diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 28b61e123..03c567069 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -18,8 +18,8 @@ import { and, eq, Param, type QueryWithTypings, SQL, sql, type SQLChunk } from ' import { Subquery, SubqueryConfig } from '~/subquery.ts'; import { getTableName, Table } from '~/table.ts'; import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; -import { View, ViewBaseConfig } from '~/view.ts'; -import { DrizzleError } from '../index.ts'; +import { View } from '~/view.ts'; +import { DrizzleError, ViewBaseConfig } from '../index.ts'; import { MySqlColumn } from './columns/common.ts'; import type { MySqlDeleteConfig } from './query-builders/delete.ts'; import type { MySqlInsertConfig } from './query-builders/insert.ts'; diff --git a/drizzle-orm/src/mysql-core/index.ts b/drizzle-orm/src/mysql-core/index.ts index 7120e0c5c..204e0af3c 100644 --- a/drizzle-orm/src/mysql-core/index.ts +++ b/drizzle-orm/src/mysql-core/index.ts @@ -13,4 +13,5 @@ export * from './subquery.ts'; export * from './table.ts'; export * from './unique-constraint.ts'; export * from './utils.ts'; +export * from './view-common.ts'; export * from './view.ts'; diff --git a/drizzle-orm/src/mysql-core/query-builders/query.ts b/drizzle-orm/src/mysql-core/query-builders/query.ts index c1433c963..3b28b1888 100644 --- a/drizzle-orm/src/mysql-core/query-builders/query.ts +++ b/drizzle-orm/src/mysql-core/query-builders/query.ts @@ -8,17 +8,11 @@ import { type TableRelationalConfig, type TablesRelationalConfig, } from '~/relations.ts'; -import { type Query, type QueryWithTypings, type SQL } from '~/sql/index.ts'; -import { type KnownKeysOnly } from '~/utils.ts'; -import { type MySqlDialect } from '../dialect.ts'; -import { - type Mode, - type MySqlSession, - type PreparedQueryConfig, - type PreparedQueryHKTBase, - type PreparedQueryKind, -} from '../session.ts'; -import { type MySqlTable } from '../table.ts'; +import type { Query, QueryWithTypings, SQL } from '~/sql/index.ts'; +import type { KnownKeysOnly } from '~/utils.ts'; +import type { MySqlDialect } from '../dialect.ts'; +import type { Mode, MySqlSession, PreparedQueryConfig, PreparedQueryHKTBase, PreparedQueryKind } from '../session.ts'; +import type { MySqlTable } from '../table.ts'; export class RelationalQueryBuilder< TPreparedQueryHKT extends PreparedQueryHKTBase, diff --git a/drizzle-orm/src/mysql-core/query-builders/select.ts b/drizzle-orm/src/mysql-core/query-builders/select.ts index bc65edd91..4dd4f5cda 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.ts @@ -21,7 +21,8 @@ import { SelectionProxyHandler, Subquery, SubqueryConfig } from '~/subquery.ts'; import { Table } from '~/table.ts'; import { applyMixins, getTableColumns, getTableLikeName, type ValueOrArray } from '~/utils.ts'; import { orderSelectedFields } from '~/utils.ts'; -import { type ColumnsSelection, View, ViewBaseConfig } from '~/view.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import { type ColumnsSelection, View } from '~/view.ts'; import type { CreateMySqlSelectFromBuilderMode, LockConfig, diff --git a/drizzle-orm/src/mysql-core/utils.ts b/drizzle-orm/src/mysql-core/utils.ts index 1d53694f7..f09f65f3e 100644 --- a/drizzle-orm/src/mysql-core/utils.ts +++ b/drizzle-orm/src/mysql-core/utils.ts @@ -1,6 +1,6 @@ import { is } from '~/entity.ts'; import { Table } from '~/table.ts'; -import { ViewBaseConfig } from '~/view.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; import type { Check } from './checks.ts'; import { CheckBuilder } from './checks.ts'; import type { ForeignKey } from './foreign-keys.ts'; @@ -11,8 +11,8 @@ import type { PrimaryKey } from './primary-keys.ts'; import { PrimaryKeyBuilder } from './primary-keys.ts'; import { MySqlTable } from './table.ts'; import { type UniqueConstraint, UniqueConstraintBuilder } from './unique-constraint.ts'; +import { MySqlViewConfig } from './view-common.ts'; import type { MySqlView } from './view.ts'; -import { MySqlViewConfig } from './view.ts'; export function getTableConfig(table: MySqlTable) { const columns = Object.values(table[MySqlTable.Symbol.Columns]); diff --git a/drizzle-orm/src/mysql-core/view-common.ts b/drizzle-orm/src/mysql-core/view-common.ts new file mode 100644 index 000000000..9bbc130c3 --- /dev/null +++ b/drizzle-orm/src/mysql-core/view-common.ts @@ -0,0 +1 @@ +export const MySqlViewConfig = Symbol.for('drizzle:MySqlViewConfig'); diff --git a/drizzle-orm/src/mysql-core/view.ts b/drizzle-orm/src/mysql-core/view.ts index 5c81dff21..4d0dd216f 100644 --- a/drizzle-orm/src/mysql-core/view.ts +++ b/drizzle-orm/src/mysql-core/view.ts @@ -10,6 +10,7 @@ import { type MySqlColumn, type MySqlColumnBuilderBase } from './columns/index.t import { QueryBuilder } from './query-builders/index.ts'; import type { SelectedFields } from './query-builders/select.types.ts'; import { mysqlTable } from './table.ts'; +import { MySqlViewConfig } from './view-common.ts'; export interface ViewBuilderConfig { algorithm?: 'undefined' | 'merge' | 'temptable'; @@ -163,8 +164,6 @@ export abstract class MySqlViewBase< }; } -export const MySqlViewConfig = Symbol.for('drizzle:MySqlViewConfig'); - export class MySqlView< TName extends string = string, TExisting extends boolean = boolean, diff --git a/drizzle-orm/src/pg-core/columns/array.ts b/drizzle-orm/src/pg-core/columns/array.ts index 4106f18d5..cef68999e 100644 --- a/drizzle-orm/src/pg-core/columns/array.ts +++ b/drizzle-orm/src/pg-core/columns/array.ts @@ -7,7 +7,7 @@ import type { import type { ColumnBaseConfig } from '~/column.ts'; import { entityKind, is } from '~/entity.ts'; import type { AnyPgTable } from '~/pg-core/table.ts'; -import { makePgArray, parsePgArray } from '../utils.ts'; +import { makePgArray, parsePgArray } from '../utils/array.ts'; import { PgColumn, PgColumnBuilder } from './common.ts'; export class PgArrayBuilder< diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 671ff8e55..8f274d493 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -37,7 +37,8 @@ import { import { Subquery, SubqueryConfig } from '~/subquery.ts'; import { getTableName, Table } from '~/table.ts'; import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; -import { View, ViewBaseConfig } from '~/view.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import { View } from '~/view.ts'; import type { PgSession } from './session.ts'; import { type PgMaterializedView, PgViewBase } from './view.ts'; diff --git a/drizzle-orm/src/pg-core/index.ts b/drizzle-orm/src/pg-core/index.ts index 7120e0c5c..1a80ff7ad 100644 --- a/drizzle-orm/src/pg-core/index.ts +++ b/drizzle-orm/src/pg-core/index.ts @@ -13,4 +13,6 @@ export * from './subquery.ts'; export * from './table.ts'; export * from './unique-constraint.ts'; export * from './utils.ts'; +export * from './utils/index.ts'; +export * from './view-common.ts'; export * from './view.ts'; diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index a18664906..f954f3340 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -22,7 +22,8 @@ import { Table } from '~/table.ts'; import { tracer } from '~/tracing.ts'; import { applyMixins, getTableColumns, getTableLikeName, type ValueOrArray } from '~/utils.ts'; import { orderSelectedFields } from '~/utils.ts'; -import { type ColumnsSelection, View, ViewBaseConfig } from '~/view.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import { type ColumnsSelection, View } from '~/view.ts'; import type { CreatePgSelectFromBuilderMode, LockConfig, diff --git a/drizzle-orm/src/pg-core/utils.ts b/drizzle-orm/src/pg-core/utils.ts index 87a72bb0e..325b7191a 100644 --- a/drizzle-orm/src/pg-core/utils.ts +++ b/drizzle-orm/src/pg-core/utils.ts @@ -1,14 +1,15 @@ import { is } from '~/entity.ts'; import { PgTable } from '~/pg-core/table.ts'; import { Table } from '~/table.ts'; -import { ViewBaseConfig } from '~/view.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; import { type Check, CheckBuilder } from './checks.ts'; import type { AnyPgColumn } from './columns/index.ts'; import { type ForeignKey, ForeignKeyBuilder } from './foreign-keys.ts'; import { type Index, IndexBuilder } from './indexes.ts'; import { type PrimaryKey, PrimaryKeyBuilder } from './primary-keys.ts'; import { type UniqueConstraint, UniqueConstraintBuilder } from './unique-constraint.ts'; -import { type PgMaterializedView, PgMaterializedViewConfig, type PgView, PgViewConfig } from './view.ts'; +import { PgViewConfig } from './view-common.ts'; +import { type PgMaterializedView, PgMaterializedViewConfig, type PgView } from './view.ts'; export function getTableConfig(table: TTable) { const columns = Object.values(table[Table.Symbol.Columns]); @@ -71,102 +72,6 @@ export function getMaterializedViewConfig< }; } -function parsePgArrayValue(arrayString: string, startFrom: number, inQuotes: boolean): [string, number] { - for (let i = startFrom; i < arrayString.length; i++) { - const char = arrayString[i]; - - if (char === '\\') { - i++; - continue; - } - - if (char === '"') { - return [arrayString.slice(startFrom, i).replace(/\\/g, ''), i + 1]; - } - - if (inQuotes) { - continue; - } - - if (char === ',' || char === '}') { - return [arrayString.slice(startFrom, i).replace(/\\/g, ''), i]; - } - } - - return [arrayString.slice(startFrom).replace(/\\/g, ''), arrayString.length]; -} - -export function parsePgNestedArray(arrayString: string, startFrom = 0): [any[], number] { - const result: any[] = []; - let i = startFrom; - let lastCharIsComma = false; - - while (i < arrayString.length) { - const char = arrayString[i]; - - if (char === ',') { - if (lastCharIsComma || i === startFrom) { - result.push(''); - } - lastCharIsComma = true; - i++; - continue; - } - - lastCharIsComma = false; - - if (char === '\\') { - i += 2; - continue; - } - - if (char === '"') { - const [value, startFrom] = parsePgArrayValue(arrayString, i + 1, true); - result.push(value); - i = startFrom; - continue; - } - - if (char === '}') { - return [result, i + 1]; - } - - if (char === '{') { - const [value, startFrom] = parsePgNestedArray(arrayString, i + 1); - result.push(value); - i = startFrom; - continue; - } - - const [value, newStartFrom] = parsePgArrayValue(arrayString, i, false); - result.push(value); - i = newStartFrom; - } - - return [result, i]; -} - -export function parsePgArray(arrayString: string): any[] { - const [result] = parsePgNestedArray(arrayString, 1); - return result; -} - -export function makePgArray(array: any[]): string { - return `{${ - array.map((item) => { - if (Array.isArray(item)) { - return makePgArray(item); - } - - if (typeof item === 'string' && item.includes(',')) { - return `"${item.replace(/"/g, '\\"')}"`; - } - - return `${item}`; - }).join(',') - }}`; -} - export type ColumnsWithTable< TTableName extends string, TForeignTableName extends string, diff --git a/drizzle-orm/src/pg-core/utils/array.ts b/drizzle-orm/src/pg-core/utils/array.ts new file mode 100644 index 000000000..fef4d3fb2 --- /dev/null +++ b/drizzle-orm/src/pg-core/utils/array.ts @@ -0,0 +1,95 @@ +function parsePgArrayValue(arrayString: string, startFrom: number, inQuotes: boolean): [string, number] { + for (let i = startFrom; i < arrayString.length; i++) { + const char = arrayString[i]; + + if (char === '\\') { + i++; + continue; + } + + if (char === '"') { + return [arrayString.slice(startFrom, i).replace(/\\/g, ''), i + 1]; + } + + if (inQuotes) { + continue; + } + + if (char === ',' || char === '}') { + return [arrayString.slice(startFrom, i).replace(/\\/g, ''), i]; + } + } + + return [arrayString.slice(startFrom).replace(/\\/g, ''), arrayString.length]; +} + +export function parsePgNestedArray(arrayString: string, startFrom = 0): [any[], number] { + const result: any[] = []; + let i = startFrom; + let lastCharIsComma = false; + + while (i < arrayString.length) { + const char = arrayString[i]; + + if (char === ',') { + if (lastCharIsComma || i === startFrom) { + result.push(''); + } + lastCharIsComma = true; + i++; + continue; + } + + lastCharIsComma = false; + + if (char === '\\') { + i += 2; + continue; + } + + if (char === '"') { + const [value, startFrom] = parsePgArrayValue(arrayString, i + 1, true); + result.push(value); + i = startFrom; + continue; + } + + if (char === '}') { + return [result, i + 1]; + } + + if (char === '{') { + const [value, startFrom] = parsePgNestedArray(arrayString, i + 1); + result.push(value); + i = startFrom; + continue; + } + + const [value, newStartFrom] = parsePgArrayValue(arrayString, i, false); + result.push(value); + i = newStartFrom; + } + + return [result, i]; +} + +export function parsePgArray(arrayString: string): any[] { + const [result] = parsePgNestedArray(arrayString, 1); + return result; +} + +export function makePgArray(array: any[]): string { + return `{${ + array.map((item) => { + if (Array.isArray(item)) { + return makePgArray(item); + } + + if (typeof item === 'string' && item.includes(',')) { + return `"${item.replace(/"/g, '\\"')}"`; + } + + return `${item}`; + }).join(',') + }}`; +} diff --git a/drizzle-orm/src/pg-core/utils/index.ts b/drizzle-orm/src/pg-core/utils/index.ts new file mode 100644 index 000000000..76eb91d0b --- /dev/null +++ b/drizzle-orm/src/pg-core/utils/index.ts @@ -0,0 +1 @@ +export * from './array.ts'; diff --git a/drizzle-orm/src/pg-core/view-common.ts b/drizzle-orm/src/pg-core/view-common.ts new file mode 100644 index 000000000..01194c7f2 --- /dev/null +++ b/drizzle-orm/src/pg-core/view-common.ts @@ -0,0 +1 @@ +export const PgViewConfig = Symbol.for('drizzle:PgViewConfig'); diff --git a/drizzle-orm/src/pg-core/view.ts b/drizzle-orm/src/pg-core/view.ts index 0ddfcf772..4e4eed3ce 100644 --- a/drizzle-orm/src/pg-core/view.ts +++ b/drizzle-orm/src/pg-core/view.ts @@ -10,6 +10,7 @@ import type { PgColumn, PgColumnBuilderBase } from './columns/common.ts'; import { QueryBuilder } from './query-builders/index.ts'; import type { SelectedFields } from './query-builders/select.types.ts'; import { pgTable } from './table.ts'; +import { PgViewConfig } from './view-common.ts'; export interface ViewWithConfig { checkOption: 'local' | 'cascaded'; @@ -282,8 +283,6 @@ export abstract class PgViewBase< }; } -export const PgViewConfig = Symbol.for('drizzle:PgViewConfig'); - export class PgView< TName extends string = string, TExisting extends boolean = boolean, diff --git a/drizzle-orm/src/sql/expressions/conditions.ts b/drizzle-orm/src/sql/expressions/conditions.ts index c84383703..774760366 100644 --- a/drizzle-orm/src/sql/expressions/conditions.ts +++ b/drizzle-orm/src/sql/expressions/conditions.ts @@ -12,7 +12,7 @@ import { type SQLChunk, type SQLWrapper, StringChunk, -} from '../index.ts'; +} from '../sql.ts'; export function bindIfParam(value: unknown, column: SQLWrapper): SQLChunk { if ( diff --git a/drizzle-orm/src/sql/expressions/select.ts b/drizzle-orm/src/sql/expressions/select.ts index 4308454cd..2ff8de346 100644 --- a/drizzle-orm/src/sql/expressions/select.ts +++ b/drizzle-orm/src/sql/expressions/select.ts @@ -1,6 +1,6 @@ import type { AnyColumn } from '../../column.ts'; -import type { SQL, SQLWrapper } from '../index.ts'; -import { sql } from '../index.ts'; +import type { SQL, SQLWrapper } from '../sql.ts'; +import { sql } from '../sql.ts'; /** * Used in sorting, this specifies that the given diff --git a/drizzle-orm/src/sql/index.ts b/drizzle-orm/src/sql/index.ts index 9a611c861..ec7f9ed76 100644 --- a/drizzle-orm/src/sql/index.ts +++ b/drizzle-orm/src/sql/index.ts @@ -1,585 +1,2 @@ -import { entityKind, is } from '~/entity.ts'; -import { Relation } from '~/relations.ts'; -import { Subquery, SubqueryConfig } from '~/subquery.ts'; -import { tracer } from '~/tracing.ts'; -import { View, ViewBaseConfig } from '~/view.ts'; -import type { AnyColumn } from '../column.ts'; -import { Column } from '../column.ts'; -import { Table } from '../table.ts'; - export * from './expressions/index.ts'; - -/** - * This class is used to indicate a primitive param value that is used in `sql` tag. - * It is only used on type level and is never instantiated at runtime. - * If you see a value of this type in the code, its runtime value is actually the primitive param value. - */ -export class FakePrimitiveParam { - static readonly [entityKind]: string = 'FakePrimitiveParam'; -} - -export type Chunk = - | string - | Table - | View - | AnyColumn - | Name - | Param - | Placeholder - | SQL; - -export interface BuildQueryConfig { - escapeName(name: string): string; - escapeParam(num: number, value: unknown): string; - escapeString(str: string): string; - prepareTyping?: (encoder: DriverValueEncoder) => QueryTypingsValue; - paramStartIndex?: { value: number }; - inlineParams?: boolean; -} - -export type QueryTypingsValue = 'json' | 'decimal' | 'time' | 'timestamp' | 'uuid' | 'date' | 'none'; - -export interface Query { - sql: string; - params: unknown[]; -} - -export interface QueryWithTypings extends Query { - typings?: QueryTypingsValue[]; -} - -/** - * Any value that implements the `getSQL` method. The implementations include: - * - `Table` - * - `Column` - * - `View` - * - `Subquery` - * - `SQL` - * - `SQL.Aliased` - * - `Placeholder` - * - `Param` - */ -export interface SQLWrapper { - getSQL(): SQL; -} - -export function isSQLWrapper(value: unknown): value is SQLWrapper { - return typeof value === 'object' && value !== null && 'getSQL' in value - && typeof (value as any).getSQL === 'function'; -} - -function mergeQueries(queries: QueryWithTypings[]): QueryWithTypings { - const result: QueryWithTypings = { sql: '', params: [] }; - for (const query of queries) { - result.sql += query.sql; - result.params.push(...query.params); - if (query.typings?.length) { - if (!result.typings) { - result.typings = []; - } - result.typings.push(...query.typings); - } - } - return result; -} - -export class StringChunk implements SQLWrapper { - static readonly [entityKind]: string = 'StringChunk'; - - readonly value: string[]; - - constructor(value: string | string[]) { - this.value = Array.isArray(value) ? value : [value]; - } - - getSQL(): SQL { - return new SQL([this]); - } -} - -export class SQL implements SQLWrapper { - static readonly [entityKind]: string = 'SQL'; - - declare _: { - brand: 'SQL'; - type: T; - }; - - /** @internal */ - decoder: DriverValueDecoder = noopDecoder; - private shouldInlineParams = false; - - constructor(readonly queryChunks: SQLChunk[]) {} - - append(query: SQL): this { - this.queryChunks.push(...query.queryChunks); - return this; - } - - toQuery(config: BuildQueryConfig): QueryWithTypings { - return tracer.startActiveSpan('drizzle.buildSQL', (span) => { - const query = this.buildQueryFromSourceParams(this.queryChunks, config); - span?.setAttributes({ - 'drizzle.query.text': query.sql, - 'drizzle.query.params': JSON.stringify(query.params), - }); - return query; - }); - } - - buildQueryFromSourceParams(chunks: SQLChunk[], _config: BuildQueryConfig): Query { - const config = Object.assign({}, _config, { - inlineParams: _config.inlineParams || this.shouldInlineParams, - paramStartIndex: _config.paramStartIndex || { value: 0 }, - }); - - const { - escapeName, - escapeParam, - prepareTyping, - inlineParams, - paramStartIndex, - } = config; - - return mergeQueries(chunks.map((chunk): QueryWithTypings => { - if (is(chunk, StringChunk)) { - return { sql: chunk.value.join(''), params: [] }; - } - - if (is(chunk, Name)) { - return { sql: escapeName(chunk.value), params: [] }; - } - - if (chunk === undefined) { - return { sql: '', params: [] }; - } - - if (Array.isArray(chunk)) { - const result: SQLChunk[] = [new StringChunk('(')]; - for (const [i, p] of chunk.entries()) { - result.push(p); - if (i < chunk.length - 1) { - result.push(new StringChunk(', ')); - } - } - result.push(new StringChunk(')')); - return this.buildQueryFromSourceParams(result, config); - } - - if (is(chunk, SQL)) { - return this.buildQueryFromSourceParams(chunk.queryChunks, { - ...config, - inlineParams: inlineParams || chunk.shouldInlineParams, - }); - } - - if (is(chunk, Table)) { - const schemaName = chunk[Table.Symbol.Schema]; - const tableName = chunk[Table.Symbol.Name]; - return { - sql: schemaName === undefined - ? escapeName(tableName) - : escapeName(schemaName) + '.' + escapeName(tableName), - params: [], - }; - } - - if (is(chunk, Column)) { - return { sql: escapeName(chunk.table[Table.Symbol.Name]) + '.' + escapeName(chunk.name), params: [] }; - } - - if (is(chunk, View)) { - const schemaName = chunk[ViewBaseConfig].schema; - const viewName = chunk[ViewBaseConfig].name; - return { - sql: schemaName === undefined - ? escapeName(viewName) - : escapeName(schemaName) + '.' + escapeName(viewName), - params: [], - }; - } - - if (is(chunk, Param)) { - const mappedValue = (chunk.value === null) ? null : chunk.encoder.mapToDriverValue(chunk.value); - - if (is(mappedValue, SQL)) { - return this.buildQueryFromSourceParams([mappedValue], config); - } - - if (inlineParams) { - return { sql: this.mapInlineParam(mappedValue, config), params: [] }; - } - - let typings: QueryTypingsValue[] | undefined; - if (prepareTyping !== undefined) { - typings = [prepareTyping(chunk.encoder)]; - } - - return { sql: escapeParam(paramStartIndex.value++, mappedValue), params: [mappedValue], typings }; - } - - if (is(chunk, Placeholder)) { - return { sql: escapeParam(paramStartIndex.value++, chunk), params: [chunk] }; - } - - if (is(chunk, SQL.Aliased) && chunk.fieldAlias !== undefined) { - return { sql: escapeName(chunk.fieldAlias), params: [] }; - } - - if (is(chunk, Subquery)) { - if (chunk[SubqueryConfig].isWith) { - return { sql: escapeName(chunk[SubqueryConfig].alias), params: [] }; - } - return this.buildQueryFromSourceParams([ - new StringChunk('('), - chunk[SubqueryConfig].sql, - new StringChunk(') '), - new Name(chunk[SubqueryConfig].alias), - ], config); - } - - // if (is(chunk, Placeholder)) { - // return {sql: escapeParam} - - if (isSQLWrapper(chunk)) { - return this.buildQueryFromSourceParams([ - new StringChunk('('), - chunk.getSQL(), - new StringChunk(')'), - ], config); - } - - if (is(chunk, Relation)) { - return this.buildQueryFromSourceParams([ - chunk.sourceTable, - new StringChunk('.'), - sql.identifier(chunk.fieldName), - ], config); - } - - if (inlineParams) { - return { sql: this.mapInlineParam(chunk, config), params: [] }; - } - - return { sql: escapeParam(paramStartIndex.value++, chunk), params: [chunk] }; - })); - } - - private mapInlineParam( - chunk: unknown, - { escapeString }: BuildQueryConfig, - ): string { - if (chunk === null) { - return 'null'; - } - if (typeof chunk === 'number' || typeof chunk === 'boolean') { - return chunk.toString(); - } - if (typeof chunk === 'string') { - return escapeString(chunk); - } - if (typeof chunk === 'object') { - const mappedValueAsString = chunk.toString(); - if (mappedValueAsString === '[object Object]') { - return escapeString(JSON.stringify(chunk)); - } - return escapeString(mappedValueAsString); - } - throw new Error('Unexpected param value: ' + chunk); - } - - getSQL(): SQL { - return this; - } - - as(alias: string): SQL.Aliased; - /** - * @deprecated - * Use ``sql`query`.as(alias)`` instead. - */ - as(): SQL; - /** - * @deprecated - * Use ``sql`query`.as(alias)`` instead. - */ - as(alias: string): SQL.Aliased; - as(alias?: string): SQL | SQL.Aliased { - // TODO: remove with deprecated overloads - if (alias === undefined) { - return this; - } - - return new SQL.Aliased(this, alias); - } - - mapWith< - TDecoder extends - | DriverValueDecoder - | DriverValueDecoder['mapFromDriverValue'], - >(decoder: TDecoder): SQL> { - this.decoder = typeof decoder === 'function' ? { mapFromDriverValue: decoder } : decoder; - return this as SQL>; - } - - inlineParams(): this { - this.shouldInlineParams = true; - return this; - } -} - -export type GetDecoderResult = T extends Column ? T['_']['data'] : T extends - | DriverValueDecoder - | DriverValueDecoder['mapFromDriverValue'] ? TData -: never; - -/** - * Any DB name (table, column, index etc.) - */ -export class Name implements SQLWrapper { - static readonly [entityKind]: string = 'Name'; - - protected brand!: 'Name'; - - constructor(readonly value: string) {} - - getSQL(): SQL { - return new SQL([this]); - } -} - -/** - * Any DB name (table, column, index etc.) - * @deprecated Use `sql.identifier` instead. - */ -export function name(value: string): Name { - return new Name(value); -} - -export interface DriverValueDecoder { - mapFromDriverValue(value: TDriverParam): TData; -} - -export interface DriverValueEncoder { - mapToDriverValue(value: TData): TDriverParam | SQL; -} - -export function isDriverValueEncoder(value: unknown): value is DriverValueEncoder { - return typeof value === 'object' && value !== null && 'mapToDriverValue' in value - && typeof (value as any).mapToDriverValue === 'function'; -} - -export const noopDecoder: DriverValueDecoder = { - mapFromDriverValue: (value) => value, -}; - -export const noopEncoder: DriverValueEncoder = { - mapToDriverValue: (value) => value, -}; - -export interface DriverValueMapper - extends DriverValueDecoder, DriverValueEncoder -{} - -export const noopMapper: DriverValueMapper = { - ...noopDecoder, - ...noopEncoder, -}; - -/** Parameter value that is optionally bound to an encoder (for example, a column). */ -export class Param implements SQLWrapper { - static readonly [entityKind]: string = 'Param'; - - protected brand!: 'BoundParamValue'; - - /** - * @param value - Parameter value - * @param encoder - Encoder to convert the value to a driver parameter - */ - constructor( - readonly value: TDataType, - readonly encoder: DriverValueEncoder = noopEncoder, - ) {} - - getSQL(): SQL { - return new SQL([this]); - } -} - -/** @deprecated Use `sql.param` instead. */ -export function param( - value: TData, - encoder?: DriverValueEncoder, -): Param { - return new Param(value, encoder); -} - -/** - * Anything that can be passed to the `` sql`...` `` tagged function. - */ -export type SQLChunk = - | StringChunk - | SQLChunk[] - | SQLWrapper - | SQL - | Table - | View - | Subquery - | AnyColumn - | Param - | Name - | undefined - | FakePrimitiveParam - | Placeholder; - -export function sql(strings: TemplateStringsArray, ...params: any[]): SQL; -/* - The type of `params` is specified as `SQLSourceParam[]`, but that's slightly incorrect - - in runtime, users won't pass `FakePrimitiveParam` instances as `params` - they will pass primitive values - which will be wrapped in `Param` using `buildChunksFromParam(...)`. That's why the overload - specify `params` as `any[]` and not as `SQLSourceParam[]`. This type is used to make our lives easier and - the type checker happy. -*/ -export function sql(strings: TemplateStringsArray, ...params: SQLChunk[]): SQL { - const queryChunks: SQLChunk[] = []; - if (params.length > 0 || (strings.length > 0 && strings[0] !== '')) { - queryChunks.push(new StringChunk(strings[0]!)); - } - for (const [paramIndex, param] of params.entries()) { - queryChunks.push(param, new StringChunk(strings[paramIndex + 1]!)); - } - - return new SQL(queryChunks); -} - -export namespace sql { - export function empty(): SQL { - return new SQL([]); - } - - /** @deprecated - use `sql.join()` */ - export function fromList(list: SQLChunk[]): SQL { - return new SQL(list); - } - - /** - * Convenience function to create an SQL query from a raw string. - * @param str The raw SQL query string. - */ - export function raw(str: string): SQL { - return new SQL([new StringChunk(str)]); - } - - /** - * Join a list of SQL chunks with a separator. - * @example - * ```ts - * const query = sql.join([sql`a`, sql`b`, sql`c`]); - * // sql`abc` - * ``` - * @example - * ```ts - * const query = sql.join([sql`a`, sql`b`, sql`c`], sql`, `); - * // sql`a, b, c` - * ``` - */ - export function join(chunks: SQLChunk[], separator?: SQLChunk): SQL { - const result: SQLChunk[] = []; - for (const [i, chunk] of chunks.entries()) { - if (i > 0 && separator !== undefined) { - result.push(separator); - } - result.push(chunk); - } - return new SQL(result); - } - - /** - * Create a SQL chunk that represents a DB identifier (table, column, index etc.). - * When used in a query, the identifier will be escaped based on the DB engine. - * For example, in PostgreSQL, identifiers are escaped with double quotes. - * - * **WARNING: This function does not offer any protection against SQL injections, so you must validate any user input beforehand.** - * - * @example ```ts - * const query = sql`SELECT * FROM ${sql.identifier('my-table')}`; - * // 'SELECT * FROM "my-table"' - * ``` - */ - export function identifier(value: string): Name { - return new Name(value); - } - - export function placeholder(name: TName): Placeholder { - return new Placeholder(name); - } - - export function param( - value: TData, - encoder?: DriverValueEncoder, - ): Param { - return new Param(value, encoder); - } -} - -export namespace SQL { - export class Aliased implements SQLWrapper { - static readonly [entityKind]: string = 'SQL.Aliased'; - - declare _: { - brand: 'SQL.Aliased'; - type: T; - }; - - /** @internal */ - isSelectionField = false; - - constructor( - readonly sql: SQL, - readonly fieldAlias: string, - ) {} - - getSQL(): SQL { - return this.sql; - } - - /** @internal */ - clone() { - return new Aliased(this.sql, this.fieldAlias); - } - } -} - -export class Placeholder implements SQLWrapper { - static readonly [entityKind]: string = 'Placeholder'; - - declare protected: TValue; - - constructor(readonly name: TName) {} - - getSQL(): SQL { - return new SQL([this]); - } -} - -/** @deprecated Use `sql.placeholder` instead. */ -export function placeholder(name: TName): Placeholder { - return new Placeholder(name); -} - -export function fillPlaceholders(params: unknown[], values: Record): unknown[] { - return params.map((p) => { - if (is(p, Placeholder)) { - if (!(p.name in values)) { - throw new Error(`No value for placeholder "${p.name}" was provided`); - } - return values[p.name]; - } - - return p; - }); -} - -// Defined separately from the Column class to resolve circular dependency -Column.prototype.getSQL = function() { - return new SQL([this]); -}; +export * from './sql.ts'; diff --git a/drizzle-orm/src/sql/sql.ts b/drizzle-orm/src/sql/sql.ts new file mode 100644 index 000000000..cb92da20c --- /dev/null +++ b/drizzle-orm/src/sql/sql.ts @@ -0,0 +1,584 @@ +import { entityKind, is } from '~/entity.ts'; +import { Relation } from '~/relations.ts'; +import { Subquery, SubqueryConfig } from '~/subquery.ts'; +import { tracer } from '~/tracing.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import { View } from '~/view.ts'; +import type { AnyColumn } from '../column.ts'; +import { Column } from '../column.ts'; +import { Table } from '../table.ts'; + +/** + * This class is used to indicate a primitive param value that is used in `sql` tag. + * It is only used on type level and is never instantiated at runtime. + * If you see a value of this type in the code, its runtime value is actually the primitive param value. + */ +export class FakePrimitiveParam { + static readonly [entityKind]: string = 'FakePrimitiveParam'; +} + +export type Chunk = + | string + | Table + | View + | AnyColumn + | Name + | Param + | Placeholder + | SQL; + +export interface BuildQueryConfig { + escapeName(name: string): string; + escapeParam(num: number, value: unknown): string; + escapeString(str: string): string; + prepareTyping?: (encoder: DriverValueEncoder) => QueryTypingsValue; + paramStartIndex?: { value: number }; + inlineParams?: boolean; +} + +export type QueryTypingsValue = 'json' | 'decimal' | 'time' | 'timestamp' | 'uuid' | 'date' | 'none'; + +export interface Query { + sql: string; + params: unknown[]; +} + +export interface QueryWithTypings extends Query { + typings?: QueryTypingsValue[]; +} + +/** + * Any value that implements the `getSQL` method. The implementations include: + * - `Table` + * - `Column` + * - `View` + * - `Subquery` + * - `SQL` + * - `SQL.Aliased` + * - `Placeholder` + * - `Param` + */ +export interface SQLWrapper { + getSQL(): SQL; +} + +export function isSQLWrapper(value: unknown): value is SQLWrapper { + return typeof value === 'object' && value !== null && 'getSQL' in value + && typeof (value as any).getSQL === 'function'; +} + +function mergeQueries(queries: QueryWithTypings[]): QueryWithTypings { + const result: QueryWithTypings = { sql: '', params: [] }; + for (const query of queries) { + result.sql += query.sql; + result.params.push(...query.params); + if (query.typings?.length) { + if (!result.typings) { + result.typings = []; + } + result.typings.push(...query.typings); + } + } + return result; +} + +export class StringChunk implements SQLWrapper { + static readonly [entityKind]: string = 'StringChunk'; + + readonly value: string[]; + + constructor(value: string | string[]) { + this.value = Array.isArray(value) ? value : [value]; + } + + getSQL(): SQL { + return new SQL([this]); + } +} + +export class SQL implements SQLWrapper { + static readonly [entityKind]: string = 'SQL'; + + declare _: { + brand: 'SQL'; + type: T; + }; + + /** @internal */ + decoder: DriverValueDecoder = noopDecoder; + private shouldInlineParams = false; + + constructor(readonly queryChunks: SQLChunk[]) {} + + append(query: SQL): this { + this.queryChunks.push(...query.queryChunks); + return this; + } + + toQuery(config: BuildQueryConfig): QueryWithTypings { + return tracer.startActiveSpan('drizzle.buildSQL', (span) => { + const query = this.buildQueryFromSourceParams(this.queryChunks, config); + span?.setAttributes({ + 'drizzle.query.text': query.sql, + 'drizzle.query.params': JSON.stringify(query.params), + }); + return query; + }); + } + + buildQueryFromSourceParams(chunks: SQLChunk[], _config: BuildQueryConfig): Query { + const config = Object.assign({}, _config, { + inlineParams: _config.inlineParams || this.shouldInlineParams, + paramStartIndex: _config.paramStartIndex || { value: 0 }, + }); + + const { + escapeName, + escapeParam, + prepareTyping, + inlineParams, + paramStartIndex, + } = config; + + return mergeQueries(chunks.map((chunk): QueryWithTypings => { + if (is(chunk, StringChunk)) { + return { sql: chunk.value.join(''), params: [] }; + } + + if (is(chunk, Name)) { + return { sql: escapeName(chunk.value), params: [] }; + } + + if (chunk === undefined) { + return { sql: '', params: [] }; + } + + if (Array.isArray(chunk)) { + const result: SQLChunk[] = [new StringChunk('(')]; + for (const [i, p] of chunk.entries()) { + result.push(p); + if (i < chunk.length - 1) { + result.push(new StringChunk(', ')); + } + } + result.push(new StringChunk(')')); + return this.buildQueryFromSourceParams(result, config); + } + + if (is(chunk, SQL)) { + return this.buildQueryFromSourceParams(chunk.queryChunks, { + ...config, + inlineParams: inlineParams || chunk.shouldInlineParams, + }); + } + + if (is(chunk, Table)) { + const schemaName = chunk[Table.Symbol.Schema]; + const tableName = chunk[Table.Symbol.Name]; + return { + sql: schemaName === undefined + ? escapeName(tableName) + : escapeName(schemaName) + '.' + escapeName(tableName), + params: [], + }; + } + + if (is(chunk, Column)) { + return { sql: escapeName(chunk.table[Table.Symbol.Name]) + '.' + escapeName(chunk.name), params: [] }; + } + + if (is(chunk, View)) { + const schemaName = chunk[ViewBaseConfig].schema; + const viewName = chunk[ViewBaseConfig].name; + return { + sql: schemaName === undefined + ? escapeName(viewName) + : escapeName(schemaName) + '.' + escapeName(viewName), + params: [], + }; + } + + if (is(chunk, Param)) { + const mappedValue = (chunk.value === null) ? null : chunk.encoder.mapToDriverValue(chunk.value); + + if (is(mappedValue, SQL)) { + return this.buildQueryFromSourceParams([mappedValue], config); + } + + if (inlineParams) { + return { sql: this.mapInlineParam(mappedValue, config), params: [] }; + } + + let typings: QueryTypingsValue[] | undefined; + if (prepareTyping !== undefined) { + typings = [prepareTyping(chunk.encoder)]; + } + + return { sql: escapeParam(paramStartIndex.value++, mappedValue), params: [mappedValue], typings }; + } + + if (is(chunk, Placeholder)) { + return { sql: escapeParam(paramStartIndex.value++, chunk), params: [chunk] }; + } + + if (is(chunk, SQL.Aliased) && chunk.fieldAlias !== undefined) { + return { sql: escapeName(chunk.fieldAlias), params: [] }; + } + + if (is(chunk, Subquery)) { + if (chunk[SubqueryConfig].isWith) { + return { sql: escapeName(chunk[SubqueryConfig].alias), params: [] }; + } + return this.buildQueryFromSourceParams([ + new StringChunk('('), + chunk[SubqueryConfig].sql, + new StringChunk(') '), + new Name(chunk[SubqueryConfig].alias), + ], config); + } + + // if (is(chunk, Placeholder)) { + // return {sql: escapeParam} + + if (isSQLWrapper(chunk)) { + return this.buildQueryFromSourceParams([ + new StringChunk('('), + chunk.getSQL(), + new StringChunk(')'), + ], config); + } + + if (is(chunk, Relation)) { + return this.buildQueryFromSourceParams([ + chunk.sourceTable, + new StringChunk('.'), + sql.identifier(chunk.fieldName), + ], config); + } + + if (inlineParams) { + return { sql: this.mapInlineParam(chunk, config), params: [] }; + } + + return { sql: escapeParam(paramStartIndex.value++, chunk), params: [chunk] }; + })); + } + + private mapInlineParam( + chunk: unknown, + { escapeString }: BuildQueryConfig, + ): string { + if (chunk === null) { + return 'null'; + } + if (typeof chunk === 'number' || typeof chunk === 'boolean') { + return chunk.toString(); + } + if (typeof chunk === 'string') { + return escapeString(chunk); + } + if (typeof chunk === 'object') { + const mappedValueAsString = chunk.toString(); + if (mappedValueAsString === '[object Object]') { + return escapeString(JSON.stringify(chunk)); + } + return escapeString(mappedValueAsString); + } + throw new Error('Unexpected param value: ' + chunk); + } + + getSQL(): SQL { + return this; + } + + as(alias: string): SQL.Aliased; + /** + * @deprecated + * Use ``sql`query`.as(alias)`` instead. + */ + as(): SQL; + /** + * @deprecated + * Use ``sql`query`.as(alias)`` instead. + */ + as(alias: string): SQL.Aliased; + as(alias?: string): SQL | SQL.Aliased { + // TODO: remove with deprecated overloads + if (alias === undefined) { + return this; + } + + return new SQL.Aliased(this, alias); + } + + mapWith< + TDecoder extends + | DriverValueDecoder + | DriverValueDecoder['mapFromDriverValue'], + >(decoder: TDecoder): SQL> { + this.decoder = typeof decoder === 'function' ? { mapFromDriverValue: decoder } : decoder; + return this as SQL>; + } + + inlineParams(): this { + this.shouldInlineParams = true; + return this; + } +} + +export type GetDecoderResult = T extends Column ? T['_']['data'] : T extends + | DriverValueDecoder + | DriverValueDecoder['mapFromDriverValue'] ? TData +: never; + +/** + * Any DB name (table, column, index etc.) + */ +export class Name implements SQLWrapper { + static readonly [entityKind]: string = 'Name'; + + protected brand!: 'Name'; + + constructor(readonly value: string) {} + + getSQL(): SQL { + return new SQL([this]); + } +} + +/** + * Any DB name (table, column, index etc.) + * @deprecated Use `sql.identifier` instead. + */ +export function name(value: string): Name { + return new Name(value); +} + +export interface DriverValueDecoder { + mapFromDriverValue(value: TDriverParam): TData; +} + +export interface DriverValueEncoder { + mapToDriverValue(value: TData): TDriverParam | SQL; +} + +export function isDriverValueEncoder(value: unknown): value is DriverValueEncoder { + return typeof value === 'object' && value !== null && 'mapToDriverValue' in value + && typeof (value as any).mapToDriverValue === 'function'; +} + +export const noopDecoder: DriverValueDecoder = { + mapFromDriverValue: (value) => value, +}; + +export const noopEncoder: DriverValueEncoder = { + mapToDriverValue: (value) => value, +}; + +export interface DriverValueMapper + extends DriverValueDecoder, DriverValueEncoder +{} + +export const noopMapper: DriverValueMapper = { + ...noopDecoder, + ...noopEncoder, +}; + +/** Parameter value that is optionally bound to an encoder (for example, a column). */ +export class Param implements SQLWrapper { + static readonly [entityKind]: string = 'Param'; + + protected brand!: 'BoundParamValue'; + + /** + * @param value - Parameter value + * @param encoder - Encoder to convert the value to a driver parameter + */ + constructor( + readonly value: TDataType, + readonly encoder: DriverValueEncoder = noopEncoder, + ) {} + + getSQL(): SQL { + return new SQL([this]); + } +} + +/** @deprecated Use `sql.param` instead. */ +export function param( + value: TData, + encoder?: DriverValueEncoder, +): Param { + return new Param(value, encoder); +} + +/** + * Anything that can be passed to the `` sql`...` `` tagged function. + */ +export type SQLChunk = + | StringChunk + | SQLChunk[] + | SQLWrapper + | SQL + | Table + | View + | Subquery + | AnyColumn + | Param + | Name + | undefined + | FakePrimitiveParam + | Placeholder; + +export function sql(strings: TemplateStringsArray, ...params: any[]): SQL; +/* + The type of `params` is specified as `SQLSourceParam[]`, but that's slightly incorrect - + in runtime, users won't pass `FakePrimitiveParam` instances as `params` - they will pass primitive values + which will be wrapped in `Param` using `buildChunksFromParam(...)`. That's why the overload + specify `params` as `any[]` and not as `SQLSourceParam[]`. This type is used to make our lives easier and + the type checker happy. +*/ +export function sql(strings: TemplateStringsArray, ...params: SQLChunk[]): SQL { + const queryChunks: SQLChunk[] = []; + if (params.length > 0 || (strings.length > 0 && strings[0] !== '')) { + queryChunks.push(new StringChunk(strings[0]!)); + } + for (const [paramIndex, param] of params.entries()) { + queryChunks.push(param, new StringChunk(strings[paramIndex + 1]!)); + } + + return new SQL(queryChunks); +} + +export namespace sql { + export function empty(): SQL { + return new SQL([]); + } + + /** @deprecated - use `sql.join()` */ + export function fromList(list: SQLChunk[]): SQL { + return new SQL(list); + } + + /** + * Convenience function to create an SQL query from a raw string. + * @param str The raw SQL query string. + */ + export function raw(str: string): SQL { + return new SQL([new StringChunk(str)]); + } + + /** + * Join a list of SQL chunks with a separator. + * @example + * ```ts + * const query = sql.join([sql`a`, sql`b`, sql`c`]); + * // sql`abc` + * ``` + * @example + * ```ts + * const query = sql.join([sql`a`, sql`b`, sql`c`], sql`, `); + * // sql`a, b, c` + * ``` + */ + export function join(chunks: SQLChunk[], separator?: SQLChunk): SQL { + const result: SQLChunk[] = []; + for (const [i, chunk] of chunks.entries()) { + if (i > 0 && separator !== undefined) { + result.push(separator); + } + result.push(chunk); + } + return new SQL(result); + } + + /** + * Create a SQL chunk that represents a DB identifier (table, column, index etc.). + * When used in a query, the identifier will be escaped based on the DB engine. + * For example, in PostgreSQL, identifiers are escaped with double quotes. + * + * **WARNING: This function does not offer any protection against SQL injections, so you must validate any user input beforehand.** + * + * @example ```ts + * const query = sql`SELECT * FROM ${sql.identifier('my-table')}`; + * // 'SELECT * FROM "my-table"' + * ``` + */ + export function identifier(value: string): Name { + return new Name(value); + } + + export function placeholder(name: TName): Placeholder { + return new Placeholder(name); + } + + export function param( + value: TData, + encoder?: DriverValueEncoder, + ): Param { + return new Param(value, encoder); + } +} + +export namespace SQL { + export class Aliased implements SQLWrapper { + static readonly [entityKind]: string = 'SQL.Aliased'; + + declare _: { + brand: 'SQL.Aliased'; + type: T; + }; + + /** @internal */ + isSelectionField = false; + + constructor( + readonly sql: SQL, + readonly fieldAlias: string, + ) {} + + getSQL(): SQL { + return this.sql; + } + + /** @internal */ + clone() { + return new Aliased(this.sql, this.fieldAlias); + } + } +} + +export class Placeholder implements SQLWrapper { + static readonly [entityKind]: string = 'Placeholder'; + + declare protected: TValue; + + constructor(readonly name: TName) {} + + getSQL(): SQL { + return new SQL([this]); + } +} + +/** @deprecated Use `sql.placeholder` instead. */ +export function placeholder(name: TName): Placeholder { + return new Placeholder(name); +} + +export function fillPlaceholders(params: unknown[], values: Record): unknown[] { + return params.map((p) => { + if (is(p, Placeholder)) { + if (!(p.name in values)) { + throw new Error(`No value for placeholder "${p.name}" was provided`); + } + return values[p.name]; + } + + return p; + }); +} + +// Defined separately from the Column class to resolve circular dependency +Column.prototype.getSQL = function() { + return new SQL([this]); +}; diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 2744459ed..c6e64da60 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -23,7 +23,7 @@ import { SQLiteTable } from '~/sqlite-core/table.ts'; import { Subquery, SubqueryConfig } from '~/subquery.ts'; import { getTableName, Table } from '~/table.ts'; import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; -import { ViewBaseConfig } from '~/view.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; import type { SelectedFieldsOrdered, SQLiteSelectConfig, diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.ts b/drizzle-orm/src/sqlite-core/query-builders/select.ts index 0502b88bf..a8e0f72bf 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.ts @@ -20,7 +20,8 @@ import type { SQLiteTable } from '~/sqlite-core/table.ts'; import { SelectionProxyHandler, Subquery, SubqueryConfig } from '~/subquery.ts'; import { Table } from '~/table.ts'; import { applyMixins, getTableColumns, getTableLikeName, orderSelectedFields, type ValueOrArray } from '~/utils.ts'; -import { type ColumnsSelection, View, ViewBaseConfig } from '~/view.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; +import { type ColumnsSelection, View } from '~/view.ts'; import { SQLiteViewBase } from '../view.ts'; import type { CreateSQLiteSelectFromBuilderMode, diff --git a/drizzle-orm/src/sqlite-core/unique-constraint.ts b/drizzle-orm/src/sqlite-core/unique-constraint.ts index 136280d79..83dfcebdd 100644 --- a/drizzle-orm/src/sqlite-core/unique-constraint.ts +++ b/drizzle-orm/src/sqlite-core/unique-constraint.ts @@ -1,5 +1,5 @@ import { entityKind } from '~/entity.ts'; -import { type SQLiteColumn } from './columns/index.ts'; +import type { SQLiteColumn } from './columns/common.ts'; import { SQLiteTable } from './table.ts'; export function uniqueKeyName(table: SQLiteTable, columns: string[]) { diff --git a/drizzle-orm/src/sqlite-core/utils.ts b/drizzle-orm/src/sqlite-core/utils.ts index 0d06dc8ef..2312466c4 100644 --- a/drizzle-orm/src/sqlite-core/utils.ts +++ b/drizzle-orm/src/sqlite-core/utils.ts @@ -1,6 +1,6 @@ import { is } from '~/entity.ts'; import { Table } from '~/table.ts'; -import { ViewBaseConfig } from '~/view.ts'; +import { ViewBaseConfig } from '~/view-common.ts'; import type { Check } from './checks.ts'; import { CheckBuilder } from './checks.ts'; import type { ForeignKey } from './foreign-keys.ts'; @@ -11,7 +11,8 @@ import type { PrimaryKey } from './primary-keys.ts'; import { PrimaryKeyBuilder } from './primary-keys.ts'; import { SQLiteTable } from './table.ts'; import { type UniqueConstraint, UniqueConstraintBuilder } from './unique-constraint.ts'; -import { type SQLiteView, SQLiteViewConfig } from './view.ts'; +import { SQLiteViewConfig } from './view-common.ts'; +import type { SQLiteView } from './view.ts'; export function getTableConfig(table: TTable) { const columns = Object.values(table[SQLiteTable.Symbol.Columns]); diff --git a/drizzle-orm/src/sqlite-core/view-common.ts b/drizzle-orm/src/sqlite-core/view-common.ts new file mode 100644 index 000000000..ed1c08c96 --- /dev/null +++ b/drizzle-orm/src/sqlite-core/view-common.ts @@ -0,0 +1 @@ +export const SQLiteViewConfig = Symbol.for('drizzle:SQLiteViewConfig'); diff --git a/drizzle-orm/src/sqlite-core/view.ts b/drizzle-orm/src/sqlite-core/view.ts index 8b040ba9e..2a51c2ffa 100644 --- a/drizzle-orm/src/sqlite-core/view.ts +++ b/drizzle-orm/src/sqlite-core/view.ts @@ -7,9 +7,10 @@ import { SelectionProxyHandler } from '~/subquery.ts'; import { getTableColumns } from '~/utils.ts'; import { type ColumnsSelection, View } from '~/view.ts'; import type { SQLiteColumn, SQLiteColumnBuilderBase } from './columns/common.ts'; -import { QueryBuilder } from './query-builders/index.ts'; +import { QueryBuilder } from './query-builders/query-builder.ts'; import type { SelectedFields } from './query-builders/select.types.ts'; import { sqliteTable } from './table.ts'; +import { SQLiteViewConfig } from './view-common.ts'; export interface ViewBuilderConfig { algorithm?: 'undefined' | 'merge' | 'temptable'; @@ -138,8 +139,6 @@ export abstract class SQLiteViewBase< }; } -export const SQLiteViewConfig = Symbol.for('drizzle:SQLiteViewConfig'); - export class SQLiteView< TName extends string = string, TExisting extends boolean = boolean, diff --git a/drizzle-orm/src/sqlite-proxy/session.ts b/drizzle-orm/src/sqlite-proxy/session.ts index 6b0bef077..b27c6c492 100644 --- a/drizzle-orm/src/sqlite-proxy/session.ts +++ b/drizzle-orm/src/sqlite-proxy/session.ts @@ -1,7 +1,7 @@ import { entityKind } from '~/entity.ts'; import type { Logger } from '~/logger.ts'; import { NoopLogger } from '~/logger.ts'; -import { type RelationalSchemaConfig, type TablesRelationalConfig } from '~/relations.ts'; +import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; import { fillPlaceholders, type Query, sql } from '~/sql/index.ts'; import type { SQLiteAsyncDialect } from '~/sqlite-core/dialect.ts'; import { SQLiteTransaction } from '~/sqlite-core/index.ts'; @@ -13,7 +13,7 @@ import type { } from '~/sqlite-core/session.ts'; import { SQLitePreparedQuery as PreparedQueryBase, SQLiteSession } from '~/sqlite-core/session.ts'; import { mapResultRow } from '~/utils.ts'; -import { type RemoteCallback, type SqliteRemoteResult } from './driver.ts'; +import type { RemoteCallback, SqliteRemoteResult } from './driver.ts'; export interface SQLiteRemoteSessionOptions { logger?: Logger; diff --git a/drizzle-orm/src/subquery.ts b/drizzle-orm/src/subquery.ts index 094a075be..8e1413528 100644 --- a/drizzle-orm/src/subquery.ts +++ b/drizzle-orm/src/subquery.ts @@ -2,7 +2,8 @@ import { ColumnAliasProxyHandler, TableAliasProxyHandler } from './alias.ts'; import { Column } from './column.ts'; import { entityKind, is } from './entity.ts'; import { SQL, type SQLWrapper } from './sql/index.ts'; -import { type ColumnsSelection, View, ViewBaseConfig } from './view.ts'; +import { ViewBaseConfig } from './view-common.ts'; +import { type ColumnsSelection, View } from './view.ts'; export const SubqueryConfig = Symbol.for('drizzle:SubqueryConfig'); diff --git a/drizzle-orm/src/table.ts b/drizzle-orm/src/table.ts index 82e34f4e6..5ba5bcddb 100644 --- a/drizzle-orm/src/table.ts +++ b/drizzle-orm/src/table.ts @@ -1,8 +1,8 @@ import type { Column, GetColumnData } from './column.ts'; import { entityKind } from './entity.ts'; import type { OptionalKeyOnly, RequiredKeyOnly } from './operations.ts'; -import { SQL, type SQLWrapper } from './sql/index.ts'; -import { type Simplify, type Update } from './utils.ts'; +import { SQL, type SQLWrapper } from './sql/sql.ts'; +import type { Simplify, Update } from './utils.ts'; export interface TableConfig> { name: string; diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index dc71112eb..a2adc01f6 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -8,7 +8,8 @@ import { Param, SQL } from './sql/index.ts'; import type { DriverValueDecoder } from './sql/index.ts'; import { Subquery, SubqueryConfig } from './subquery.ts'; import { getTableName, Table } from './table.ts'; -import { View, ViewBaseConfig } from './view.ts'; +import { ViewBaseConfig } from './view-common.ts'; +import { View } from './view.ts'; /** @internal */ export function mapResultRow( diff --git a/drizzle-orm/src/view-common.ts b/drizzle-orm/src/view-common.ts new file mode 100644 index 000000000..f1537fb1b --- /dev/null +++ b/drizzle-orm/src/view-common.ts @@ -0,0 +1 @@ +export const ViewBaseConfig = Symbol.for('drizzle:ViewBaseConfig'); diff --git a/drizzle-orm/src/view.ts b/drizzle-orm/src/view.ts index 28b912432..bd1485bda 100644 --- a/drizzle-orm/src/view.ts +++ b/drizzle-orm/src/view.ts @@ -3,8 +3,7 @@ import { entityKind } from './entity.ts'; import type { SelectedFields } from './operations.ts'; import { SQL, type SQLWrapper } from './sql/index.ts'; import type { Table } from './table.ts'; - -export const ViewBaseConfig = Symbol.for('drizzle:ViewBaseConfig'); +import { ViewBaseConfig } from './view-common.ts'; export type ColumnsSelection = Record; diff --git a/drizzle-typebox/tests/utils.ts b/drizzle-typebox/tests/utils.ts index a98bfd961..0454dd48d 100644 --- a/drizzle-typebox/tests/utils.ts +++ b/drizzle-typebox/tests/utils.ts @@ -1,5 +1,5 @@ import type { TSchema } from '@sinclair/typebox'; -import { type ExecutionContext } from 'ava'; +import type { ExecutionContext } from 'ava'; export function expectSchemaShape(t: ExecutionContext, expected: T) { return { @@ -15,4 +15,3 @@ export function expectSchemaShape(t: ExecutionContext, expect }, }; } - diff --git a/drizzle-valibot/tests/utils.ts b/drizzle-valibot/tests/utils.ts index 1464847cb..0d38bc534 100644 --- a/drizzle-valibot/tests/utils.ts +++ b/drizzle-valibot/tests/utils.ts @@ -1,5 +1,5 @@ import { type ExecutionContext } from 'ava'; -import { type BaseSchema } from 'valibot'; +import type { BaseSchema } from 'valibot'; export function expectSchemaShape>(t: ExecutionContext, expected: T) { return { @@ -8,4 +8,3 @@ export function expectSchemaShape>(t: ExecutionCo }, }; } - diff --git a/drizzle-zod/tests/utils.ts b/drizzle-zod/tests/utils.ts index c30c9b243..b8daf972e 100644 --- a/drizzle-zod/tests/utils.ts +++ b/drizzle-zod/tests/utils.ts @@ -1,4 +1,4 @@ -import { type ExecutionContext } from 'ava'; +import type { ExecutionContext } from 'ava'; import type { z } from 'zod'; export function expectSchemaShape(t: ExecutionContext, expected: z.ZodObject) {