diff --git a/drizzle-orm/src/singlestore-core/db.ts b/drizzle-orm/src/singlestore-core/db.ts index e8b796363..5574ad14a 100644 --- a/drizzle-orm/src/singlestore-core/db.ts +++ b/drizzle-orm/src/singlestore-core/db.ts @@ -8,6 +8,7 @@ import { WithSubquery } from '~/subquery.ts'; import type { DrizzleTypeError } from '~/utils.ts'; import type { SingleStoreDialect } from './dialect.ts'; import { SingleStoreAttachBase } from './query-builders/attach.ts'; +import { SingleStoreBranchBase } from './query-builders/branch.ts'; import { SingleStoreDetachBase } from './query-builders/detach.ts'; import { QueryBuilder, @@ -487,6 +488,13 @@ export class SingleStoreDatabase< ): SingleStoreAttachBase { return new SingleStoreAttachBase(database, this.session, this.dialect); } + + branch( + database: TDatabase, + branchName: string, + ): SingleStoreBranchBase { + return new SingleStoreBranchBase(database, branchName, this.session, this.dialect); + } } export type SingleStoreWithReplicas = Q & { $primary: Q }; diff --git a/drizzle-orm/src/singlestore-core/dialect.ts b/drizzle-orm/src/singlestore-core/dialect.ts index bf3961157..e2784a16d 100644 --- a/drizzle-orm/src/singlestore-core/dialect.ts +++ b/drizzle-orm/src/singlestore-core/dialect.ts @@ -24,6 +24,7 @@ import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; import { ViewBaseConfig } from '~/view-common.ts'; import { SingleStoreColumn } from './columns/common.ts'; import type { SingleStoreAttachConfig } from './query-builders/attach.ts'; +import type { SingleStoreBranchConfig } from './query-builders/branch.ts'; import type { SingleStoreDeleteConfig } from './query-builders/delete.ts'; import type { SingleStoreDetachConfig } from './query-builders/detach.ts'; import type { SingleStoreInsertConfig } from './query-builders/insert.ts'; @@ -126,12 +127,27 @@ export class SingleStoreDialect { return sql`detach database ${database}${milestoneSql}${workspaceSql}`; } - buildAttachQuery({ database, milestone, time, databaseAlias }: SingleStoreAttachConfig): SQL { + buildAttachQuery({ database, milestone, time, databaseAlias, readOnly }: SingleStoreAttachConfig): SQL { const asSql = databaseAlias ? sql` as ${sql.identifier(databaseAlias)}` : undefined; const milestoneSql = milestone ? sql` at milestone ${milestone}` : undefined; const timeSql = time ? sql` at time ${time}` : undefined; + const readOnlySql = readOnly ? sql` READ ONLY` : undefined; - return sql`attach database ${sql.raw(database)}${asSql}${milestoneSql}${timeSql}`; + return sql`attach database ${sql.raw(database)}${readOnlySql}${asSql}${milestoneSql}${timeSql}`; + } + + buildBranchQuery( + { database, databaseAlias, fromWorkspaceGroup, milestone, readOnly, time }: SingleStoreBranchConfig, + ): SQL { + const asSql = sql` as ${sql.identifier(databaseAlias)}`; + const milestoneSql = milestone ? sql` at milestone ${milestone}` : undefined; + const timeSql = time ? sql` at time ${time}` : undefined; + const fromWorkspaceGroupSql = fromWorkspaceGroup ? sql` from workspace group ${fromWorkspaceGroup}` : undefined; + const readOnlySql = readOnly ? sql` READ ONLY` : undefined; + + return sql`attach database ${ + sql.raw(database) + }${fromWorkspaceGroupSql}${readOnlySql}${asSql}${milestoneSql}${timeSql}`; } buildUpdateSet(table: SingleStoreTable, set: UpdateSet): SQL { diff --git a/drizzle-orm/src/singlestore-core/query-builders/attach.ts b/drizzle-orm/src/singlestore-core/query-builders/attach.ts index 3c79d9adf..63416693e 100644 --- a/drizzle-orm/src/singlestore-core/query-builders/attach.ts +++ b/drizzle-orm/src/singlestore-core/query-builders/attach.ts @@ -40,6 +40,7 @@ export interface SingleStoreAttachConfig { time?: Date | undefined; database: string; databaseAlias?: string | undefined; + readOnly?: boolean | undefined; } export type SingleStoreAttachPrepare = PreparedQueryKind< @@ -149,6 +150,12 @@ export class SingleStoreAttachBase< return this as any; } + // TODO(singlestore): docs + readOnly(): SingleStoreAttachWithout { + this.config.readOnly = true; + return this as any; + } + /** @internal */ getSQL(): SQL { return this.dialect.buildAttachQuery(this.config); diff --git a/drizzle-orm/src/singlestore-core/query-builders/branch.ts b/drizzle-orm/src/singlestore-core/query-builders/branch.ts new file mode 100644 index 000000000..baa1ea247 --- /dev/null +++ b/drizzle-orm/src/singlestore-core/query-builders/branch.ts @@ -0,0 +1,195 @@ +import { entityKind } from '~/entity.ts'; +import { DrizzleError } from '~/errors.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { SingleStoreDialect } from '~/singlestore-core/dialect.ts'; +import type { + AnySingleStoreQueryResultHKT, + PreparedQueryHKTBase, + PreparedQueryKind, + SingleStorePreparedQueryConfig, + SingleStoreQueryResultHKT, + SingleStoreQueryResultKind, + SingleStoreSession, +} from '~/singlestore-core/session.ts'; +import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts'; + +export type SingleStoreBranchWithout< + T extends AnySingleStoreBranchBase, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true ? T + : Omit< + SingleStoreBranchBase< + T['_']['database'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'], + TDynamic, + T['_']['excludedMethods'] | K + >, + T['_']['excludedMethods'] | K + >; + +export type SingleStoreBranch< + TDatabase extends string = string, + TQueryResult extends SingleStoreQueryResultHKT = AnySingleStoreQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, +> = SingleStoreBranchBase; + +export interface SingleStoreBranchConfig { + milestone?: string | undefined; + time?: Date | undefined; + database: string; + databaseAlias: string; + fromWorkspaceGroup?: string | undefined; + readOnly?: boolean | undefined; +} + +export type SingleStoreBranchPrepare = PreparedQueryKind< + T['_']['preparedQueryHKT'], + SingleStorePreparedQueryConfig & { + execute: SingleStoreQueryResultKind; + iterator: never; + }, + true +>; + +type SingleStoreBranchDynamic = SingleStoreBranch< + T['_']['database'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'] +>; + +type AnySingleStoreBranchBase = SingleStoreBranchBase; + +export interface SingleStoreBranchBase< + TDatabase extends string, + TQueryResult extends SingleStoreQueryResultHKT, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, +> extends QueryPromise> { + readonly _: { + readonly database: TDatabase; + readonly queryResult: TQueryResult; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + }; +} + +export class SingleStoreBranchBase< + TDatabase extends string, + TQueryResult extends SingleStoreQueryResultHKT, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TPreparedQueryHKT extends PreparedQueryHKTBase, + TDynamic extends boolean = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TExcludedMethods extends string = never, +> extends QueryPromise> implements SQLWrapper { + static readonly [entityKind]: string = 'SingleStoreBranch'; + + private config: SingleStoreBranchConfig; + + constructor( + private database: TDatabase, + private branchName: string, + private session: SingleStoreSession, + private dialect: SingleStoreDialect, + ) { + super(); + this.config = { database, databaseAlias: branchName }; + } + + /** + * Adds a `where` clause to the query. + * + * Calling this method will delete only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param where the `where` clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be deleted. + * + * ```ts + * // Attach all cars with green color + * db.delete(cars).where(eq(cars.color, 'green')); + * // or + * db.delete(cars).where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Attach all BMW cars with a green color + * db.delete(cars).where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Attach all cars with the green or blue color + * db.delete(cars).where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ + // TODO(singlestore): docs + atMilestone(milestone: string): SingleStoreBranchWithout { + if (this.config.time) { + throw new DrizzleError({ message: 'Cannot set both time and milestone' }); + } + this.config.milestone = milestone; + return this as any; + } + + // TODO(singlestore): docs + atTime(time: Date): SingleStoreBranchWithout { + if (this.config.milestone) { + throw new DrizzleError({ message: 'Cannot set both time and milestone' }); + } + this.config.time = time; + return this as any; + } + + // TODO(singlestore): docs + fromWorkspaceGroup(groupID: string): SingleStoreBranchWithout { + this.config.fromWorkspaceGroup = groupID; + return this as any; + } + + // TODO(singlestore): docs + readOnly(): SingleStoreBranchWithout { + this.config.readOnly = true; + return this as any; + } + + /** @internal */ + getSQL(): SQL { + return this.dialect.buildBranchQuery(this.config); + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + prepare(): SingleStoreBranchPrepare { + return this.session.prepareQuery( + this.dialect.sqlToQuery(this.getSQL()), + undefined, + ) as SingleStoreBranchPrepare; + } + + override execute: ReturnType['execute'] = (placeholderValues) => { + return this.prepare().execute(placeholderValues); + }; + + private createIterator = (): ReturnType['iterator'] => { + const self = this; + return async function*(placeholderValues) { + yield* self.prepare().iterator(placeholderValues); + }; + }; + + iterator = this.createIterator(); + + $dynamic(): SingleStoreBranchDynamic { + return this as any; + } +}