diff --git a/drizzle-kit/src/api.ts b/drizzle-kit/src/api.ts index 2ac44f5b7..3922da088 100644 --- a/drizzle-kit/src/api.ts +++ b/drizzle-kit/src/api.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'; import { LibSQLDatabase } from 'drizzle-orm/libsql'; import type { MySql2Database } from 'drizzle-orm/mysql2'; import { PgDatabase } from 'drizzle-orm/pg-core'; -import { SingleStore2Database } from 'drizzle-orm/singlestore'; +import { SingleStoreDriverDatabase } from 'drizzle-orm/singlestore'; import { columnsResolver, enumsResolver, @@ -394,7 +394,7 @@ export const generateSingleStoreMigration = async ( export const pushSingleStoreSchema = async ( imports: Record, - drizzleInstance: SingleStore2Database, + drizzleInstance: SingleStoreDriverDatabase, databaseName: string, ) => { const { applySingleStoreSnapshotsDiff } = await import('./snapshotsDiffer'); diff --git a/drizzle-orm/src/monodriver.ts b/drizzle-orm/src/monodriver.ts index 03fcdc58f..612da1bdc 100644 --- a/drizzle-orm/src/monodriver.ts +++ b/drizzle-orm/src/monodriver.ts @@ -30,7 +30,7 @@ import type { NeonDatabase } from './neon-serverless/index.ts'; import type { NodePgDatabase } from './node-postgres/index.ts'; import type { PlanetScaleDatabase } from './planetscale-serverless/index.ts'; import type { PostgresJsDatabase } from './postgres-js/index.ts'; -import type { SingleStore2Database, SingleStore2DrizzleConfig } from './singlestore/driver.ts'; +import type { SingleStoreDriverDatabase, SingleStoreDriverDrizzleConfig } from './singlestore/driver.ts'; import type { TiDBServerlessDatabase } from './tidb-serverless/index.ts'; import type { DrizzleConfig } from './utils.ts'; import type { VercelPgDatabase } from './vercel-postgres/index.ts'; @@ -116,7 +116,7 @@ type ClientDrizzleInstanceMap> = { d1: DrizzleD1Database; 'bun:sqlite': BunSQLiteDatabase; 'better-sqlite3': DrizzleBetterSQLite3Database; - singlestore: SingleStore2Database; + singlestore: SingleStoreDriverDatabase; }; type ClientInstanceMap = { @@ -138,7 +138,7 @@ type ClientInstanceMap = { d1: D1Database; 'bun:sqlite': BunDatabase; 'better-sqlite3': BetterSQLite3Database; - singlestore: SingleStore2Database; + singlestore: SingleStoreDriverDatabase; }; type InitializerParams = { @@ -220,6 +220,7 @@ export async function drizzle< : TClient extends 'neon-serverless' ? DrizzleConfig & { ws?: any; } + : TClient extends 'singlestore' ? SingleStoreDriverDrizzleConfig : DrizzleConfig), ): Promise> { const { connection, ws, ...drizzleConfig } = params as typeof params & { @@ -394,7 +395,7 @@ export async function drizzle< const instance = createPool(connection as Mysql2Config); const { drizzle } = await import('./mysql2'); - const db = drizzle(instance, drizzleConfig as SingleStore2DrizzleConfig) as any; + const db = drizzle(instance, drizzleConfig as SingleStoreDriverDrizzleConfig) as any; db.$client = instance; return db; diff --git a/drizzle-orm/src/monomigrator.ts b/drizzle-orm/src/monomigrator.ts index d8681ebf9..d113b223b 100644 --- a/drizzle-orm/src/monomigrator.ts +++ b/drizzle-orm/src/monomigrator.ts @@ -12,7 +12,7 @@ import type { NeonDatabase } from './neon-serverless/index.ts'; import type { NodePgDatabase } from './node-postgres/index.ts'; import type { PlanetScaleDatabase } from './planetscale-serverless/index.ts'; import type { PostgresJsDatabase } from './postgres-js/index.ts'; -import type { SingleStore2Database } from './singlestore/driver.ts'; +import type { SingleStoreDriverDatabase } from './singlestore/driver.ts'; import type { TiDBServerlessDatabase } from './tidb-serverless/index.ts'; import type { VercelPgDatabase } from './vercel-postgres/index.ts'; @@ -31,7 +31,7 @@ export async function migrate( | PostgresJsDatabase | VercelPgDatabase | TiDBServerlessDatabase - | SingleStore2Database, + | SingleStoreDriverDatabase, config: | string | MigrationConfig, @@ -102,10 +102,10 @@ export async function migrate( return migrate(db as VercelPgDatabase, config as string | MigrationConfig); } - case 'SingleStore2Database': { + case 'SingleStoreDriverDatabase': { const { migrate } = await import('./singlestore/migrator'); - return migrate(db as SingleStore2Database, config as MigrationConfig); + return migrate(db as SingleStoreDriverDatabase, config as MigrationConfig); } } } diff --git a/drizzle-orm/src/singlestore-core/db.ts b/drizzle-orm/src/singlestore-core/db.ts index 7c2c16ec4..581b3fa1b 100644 --- a/drizzle-orm/src/singlestore-core/db.ts +++ b/drizzle-orm/src/singlestore-core/db.ts @@ -24,7 +24,6 @@ import type { OptimizeTableArgument } from './query-builders/optimizeTable.types import { RelationalQueryBuilder } from './query-builders/query.ts'; import type { SelectedFields } from './query-builders/select.types.ts'; import type { - Mode, PreparedQueryHKTBase, SingleStoreQueryResultHKT, SingleStoreQueryResultKind, @@ -61,7 +60,6 @@ export class SingleStoreDatabase< /** @internal */ readonly session: SingleStoreSession, schema: RelationalSchemaConfig | undefined, - protected readonly mode: Mode, ) { this._ = schema ? { @@ -86,7 +84,6 @@ export class SingleStoreDatabase< columns, dialect, session, - this.mode, ); } } diff --git a/drizzle-orm/src/singlestore-core/dialect.ts b/drizzle-orm/src/singlestore-core/dialect.ts index 7ae21b418..dd7bdc89f 100644 --- a/drizzle-orm/src/singlestore-core/dialect.ts +++ b/drizzle-orm/src/singlestore-core/dialect.ts @@ -849,299 +849,4 @@ export class SingleStoreDialect { selection, }; } - - buildRelationalQueryWithoutLateralSubqueries({ - fullSchema, - schema, - tableNamesMap, - table, - tableConfig, - queryConfig: config, - tableAlias, - nestedQueryRelation, - joinOn, - }: { - fullSchema: Record; - schema: TablesRelationalConfig; - tableNamesMap: Record; - table: SingleStoreTable; - tableConfig: TableRelationalConfig; - queryConfig: true | DBQueryConfig<'many', true>; - tableAlias: string; - nestedQueryRelation?: Relation; - joinOn?: SQL; - }): BuildRelationalQueryResult { - let selection: BuildRelationalQueryResult['selection'] = []; - let limit, offset, orderBy: SingleStoreSelectConfig['orderBy'] = [], where; - - if (config === true) { - const selectionEntries = Object.entries(tableConfig.columns); - selection = selectionEntries.map(( - [key, value], - ) => ({ - dbKey: value.name, - tsKey: key, - field: aliasedTableColumn(value as SingleStoreColumn, 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: SingleStoreColumn | SQL.Aliased }[] = []; - let selectedColumns: string[] = []; - - // Figure out which columns to select - if (config.columns) { - let isIncludeMode = false; - - for (const [field, value] of Object.entries(config.columns)) { - if (value === undefined) { - continue; - } - - if (field in tableConfig.columns) { - if (!isIncludeMode && value === true) { - isIncludeMode = true; - } - selectedColumns.push(field); - } - } - - if (selectedColumns.length > 0) { - selectedColumns = isIncludeMode - ? selectedColumns.filter((c) => config.columns?.[c] === true) - : Object.keys(tableConfig.columns).filter((key) => !selectedColumns.includes(key)); - } - } else { - // Select all columns if selection is not specified - selectedColumns = Object.keys(tableConfig.columns); - } - - for (const field of selectedColumns) { - const column = tableConfig.columns[field]! as SingleStoreColumn; - fieldsSelection.push({ tsKey: field, value: column }); - } - - let selectedRelations: { - tsKey: string; - queryConfig: true | DBQueryConfig<'many', false>; - relation: Relation; - }[] = []; - - // Figure out which relations to select - if (config.with) { - selectedRelations = Object.entries(config.with) - .filter((entry): entry is [typeof entry[0], NonNullable] => !!entry[1]) - .map(([tsKey, queryConfig]) => ({ tsKey, queryConfig, relation: tableConfig.relations[tsKey]! })); - } - - let extras; - - // Figure out which extras to select - if (config.extras) { - extras = typeof config.extras === 'function' - ? config.extras(aliasedColumns, { sql }) - : config.extras; - for (const [tsKey, value] of Object.entries(extras)) { - fieldsSelection.push({ - tsKey, - value: mapColumnsInAliasedSQLToAlias(value, tableAlias), - }); - } - } - - // Transform `fieldsSelection` into `selection` - // `fieldsSelection` shouldn't be used after this point - for (const { tsKey, value } of fieldsSelection) { - selection.push({ - dbKey: is(value, SQL.Aliased) ? value.fieldAlias : tableConfig.columns[tsKey]!.name, - tsKey, - field: is(value, Column) ? aliasedTableColumn(value, tableAlias) : value, - relationTableTsKey: undefined, - isJson: false, - selection: [], - }); - } - - let orderByOrig = typeof config.orderBy === 'function' - ? config.orderBy(aliasedColumns, getOrderByOperators()) - : config.orderBy ?? []; - if (!Array.isArray(orderByOrig)) { - orderByOrig = [orderByOrig]; - } - orderBy = orderByOrig.map((orderByValue) => { - if (is(orderByValue, Column)) { - return aliasedTableColumn(orderByValue, tableAlias) as SingleStoreColumn; - } - return mapColumnsInSQLToAlias(orderByValue, tableAlias); - }); - - limit = config.limit; - offset = config.offset; - - // Process all relations - for ( - const { - tsKey: selectedRelationTsKey, - queryConfig: selectedRelationConfigValue, - relation, - } of selectedRelations - ) { - const normalizedRelation = normalizeRelation(schema, tableNamesMap, relation); - const relationTableName = getTableUniqueName(relation.referencedTable); - const relationTableTsName = tableNamesMap[relationTableName]!; - const relationTableAlias = `${tableAlias}_${selectedRelationTsKey}`; - const joinOn = and( - ...normalizedRelation.fields.map((field, i) => - eq( - aliasedTableColumn(normalizedRelation.references[i]!, relationTableAlias), - aliasedTableColumn(field, tableAlias), - ) - ), - ); - const builtRelation = this.buildRelationalQueryWithoutLateralSubqueries({ - fullSchema, - schema, - tableNamesMap, - table: fullSchema[relationTableTsName] as SingleStoreTable, - tableConfig: schema[relationTableTsName]!, - queryConfig: is(relation, One) - ? (selectedRelationConfigValue === true - ? { limit: 1 } - : { ...selectedRelationConfigValue, limit: 1 }) - : selectedRelationConfigValue, - tableAlias: relationTableAlias, - joinOn, - nestedQueryRelation: relation, - }); - let fieldSql = sql`(${builtRelation.sql})`; - if (is(relation, Many)) { - fieldSql = sql`coalesce(${fieldSql}, "[]")`; - } - const field = fieldSql.as(selectedRelationTsKey); - selection.push({ - dbKey: selectedRelationTsKey, - tsKey: selectedRelationTsKey, - field, - relationTableTsKey: relationTableTsName, - isJson: true, - selection: builtRelation.selection, - }); - } - } - - if (selection.length === 0) { - throw new DrizzleError({ - message: - `No fields selected for table "${tableConfig.tsName}" ("${tableAlias}"). You need to have at least one item in "columns", "with" or "extras". If you need to select all columns, omit the "columns" key or set it to undefined.`, - }); - } - - let result; - - where = and(joinOn, where); - - if (nestedQueryRelation) { - let field = sql`JSON_BUILD_OBJECT(${ - sql.join( - selection.map(({ field }, index) => - is(field, SingleStoreColumn) - ? sql`${index}, ${sql.identifier(field.name)}` - : is(field, SQL.Aliased) - ? sql`${index}, ${field.sql}` - : sql`${index}, ${field}` - ), - sql`, `, - ) - })`; - if (is(nestedQueryRelation, Many)) { - field = sql`json_agg(${field})`; - } - const nestedSelection = [{ - dbKey: 'data', - tsKey: 'data', - field, - isJson: true, - relationTableTsKey: tableConfig.tsName, - selection, - }]; - - const needsSubquery = limit !== undefined || offset !== undefined || orderBy.length > 0; - - if (needsSubquery) { - result = this.buildSelectQuery({ - table: aliasedTable(table, tableAlias), - fields: {}, - fieldsFlat: [ - { - path: [], - field: sql.raw('*'), - }, - ...(orderBy.length > 0) - ? [{ - path: [], - field: sql`row_number() over (order by ${sql.join(orderBy, sql`, `)})`, - }] - : [], - ], - where, - limit, - offset, - setOperators: [], - }); - - where = undefined; - limit = undefined; - offset = undefined; - orderBy = undefined; - } else { - result = aliasedTable(table, tableAlias); - } - - result = this.buildSelectQuery({ - table: is(result, SingleStoreTable) ? result : new Subquery(result, {}, tableAlias), - fields: {}, - fieldsFlat: nestedSelection.map(({ field }) => ({ - path: [], - field: is(field, Column) ? aliasedTableColumn(field, tableAlias) : field, - })), - where, - limit, - offset, - orderBy, - setOperators: [], - }); - } else { - result = this.buildSelectQuery({ - table: aliasedTable(table, tableAlias), - fields: {}, - fieldsFlat: selection.map(({ field }) => ({ - path: [], - field: is(field, Column) ? aliasedTableColumn(field, tableAlias) : field, - })), - where, - limit, - offset, - orderBy, - setOperators: [], - }); - } - - return { - tableTsKey: tableConfig.tsName, - sql: result, - selection, - }; - } } diff --git a/drizzle-orm/src/singlestore-core/query-builders/query.ts b/drizzle-orm/src/singlestore-core/query-builders/query.ts index 40d3f05ad..5744c7c9c 100644 --- a/drizzle-orm/src/singlestore-core/query-builders/query.ts +++ b/drizzle-orm/src/singlestore-core/query-builders/query.ts @@ -12,7 +12,6 @@ import type { Query, QueryWithTypings, SQL } from '~/sql/sql.ts'; import type { KnownKeysOnly } from '~/utils.ts'; import type { SingleStoreDialect } from '../dialect.ts'; import type { - Mode, PreparedQueryHKTBase, PreparedQueryKind, SingleStorePreparedQueryConfig, @@ -35,7 +34,6 @@ export class RelationalQueryBuilder< private tableConfig: TableRelationalConfig, private dialect: SingleStoreDialect, private session: SingleStoreSession, - private mode: Mode, ) {} findMany>( @@ -51,7 +49,6 @@ export class RelationalQueryBuilder< this.session, config ? (config as DBQueryConfig<'many', true>) : {}, 'many', - this.mode, ); } @@ -68,7 +65,6 @@ export class RelationalQueryBuilder< this.session, config ? { ...(config as DBQueryConfig<'many', true> | undefined), limit: 1 } : { limit: 1 }, 'first', - this.mode, ); } } @@ -91,7 +87,6 @@ export class SingleStoreRelationalQuery< private session: SingleStoreSession, private config: DBQueryConfig<'many', true> | true, private queryMode: 'many' | 'first', - private mode?: Mode, ) { super(); } @@ -112,26 +107,15 @@ export class SingleStoreRelationalQuery< } private _getQuery() { - const query = this.mode === 'planetscale' - ? this.dialect.buildRelationalQueryWithoutLateralSubqueries({ - fullSchema: this.fullSchema, - schema: this.schema, - tableNamesMap: this.tableNamesMap, - table: this.table, - tableConfig: this.tableConfig, - queryConfig: this.config, - tableAlias: this.tableConfig.tsName, - }) - : this.dialect.buildRelationalQuery({ - fullSchema: this.fullSchema, - schema: this.schema, - tableNamesMap: this.tableNamesMap, - table: this.table, - tableConfig: this.tableConfig, - queryConfig: this.config, - tableAlias: this.tableConfig.tsName, - }); - return query; + return this.dialect.buildRelationalQuery({ + fullSchema: this.fullSchema, + schema: this.schema, + tableNamesMap: this.tableNamesMap, + table: this.table, + tableConfig: this.tableConfig, + queryConfig: this.config, + tableAlias: this.tableConfig.tsName, + }); } private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: QueryWithTypings } { diff --git a/drizzle-orm/src/singlestore-core/query-builders/select.ts b/drizzle-orm/src/singlestore-core/query-builders/select.ts index a7652412e..78cba92e0 100644 --- a/drizzle-orm/src/singlestore-core/query-builders/select.ts +++ b/drizzle-orm/src/singlestore-core/query-builders/select.ts @@ -494,48 +494,6 @@ export abstract class SingleStoreSelectQueryBuilderBase< */ intersect = this.createSetOperator('intersect', false); - /** - * Adds `intersect all` set operator to the query. - * - * Calling this method will retain only the rows that are present in both result sets including all duplicates. - * - * See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect-all} - * - * @example - * - * ```ts - * // Select all products and quantities that are ordered by both regular and VIP customers - * await db.select({ - * productId: regularCustomerOrders.productId, - * quantityOrdered: regularCustomerOrders.quantityOrdered - * }) - * .from(regularCustomerOrders) - * .intersectAll( - * db.select({ - * productId: vipCustomerOrders.productId, - * quantityOrdered: vipCustomerOrders.quantityOrdered - * }) - * .from(vipCustomerOrders) - * ); - * // or - * import { intersectAll } from 'drizzle-orm/pg-core' - * - * await intersectAll( - * db.select({ - * productId: regularCustomerOrders.productId, - * quantityOrdered: regularCustomerOrders.quantityOrdered - * }) - * .from(regularCustomerOrders), - * db.select({ - * productId: vipCustomerOrders.productId, - * quantityOrdered: vipCustomerOrders.quantityOrdered - * }) - * .from(vipCustomerOrders) - * ); - * ``` - */ - intersectAll = this.createSetOperator('intersect', true); - /** * Adds `except` set operator to the query. * @@ -563,48 +521,6 @@ export abstract class SingleStoreSelectQueryBuilderBase< */ except = this.createSetOperator('except', false); - /** - * Adds `except all` set operator to the query. - * - * Calling this method will retrieve all rows from the left query, except for the rows that are present in the result set of the right query. - * - * See docs: {@link https://orm.drizzle.team/docs/set-operations#except-all} - * - * @example - * - * ```ts - * // Select all products that are ordered by regular customers but not by VIP customers - * await db.select({ - * productId: regularCustomerOrders.productId, - * quantityOrdered: regularCustomerOrders.quantityOrdered, - * }) - * .from(regularCustomerOrders) - * .exceptAll( - * db.select({ - * productId: vipCustomerOrders.productId, - * quantityOrdered: vipCustomerOrders.quantityOrdered, - * }) - * .from(vipCustomerOrders) - * ); - * // or - * import { exceptAll } from 'drizzle-orm/pg-core' - * - * await exceptAll( - * db.select({ - * productId: regularCustomerOrders.productId, - * quantityOrdered: regularCustomerOrders.quantityOrdered - * }) - * .from(regularCustomerOrders), - * db.select({ - * productId: vipCustomerOrders.productId, - * quantityOrdered: vipCustomerOrders.quantityOrdered - * }) - * .from(vipCustomerOrders) - * ); - * ``` - */ - exceptAll = this.createSetOperator('except', true); - /** * Adds `minus` set operator to the query. * @@ -1027,9 +943,7 @@ const getSingleStoreSetOperators = () => ({ union, unionAll, intersect, - intersectAll, except, - exceptAll, minus, }); @@ -1114,48 +1028,6 @@ export const unionAll = createSetOperator('union', true); */ export const intersect = createSetOperator('intersect', false); -/** - * Adds `intersect all` set operator to the query. - * - * Calling this method will retain only the rows that are present in both result sets including all duplicates. - * - * See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect-all} - * - * @example - * - * ```ts - * // Select all products and quantities that are ordered by both regular and VIP customers - * import { intersectAll } from 'drizzle-orm/mysql-core' - * - * await intersectAll( - * db.select({ - * productId: regularCustomerOrders.productId, - * quantityOrdered: regularCustomerOrders.quantityOrdered - * }) - * .from(regularCustomerOrders), - * db.select({ - * productId: vipCustomerOrders.productId, - * quantityOrdered: vipCustomerOrders.quantityOrdered - * }) - * .from(vipCustomerOrders) - * ); - * // or - * await db.select({ - * productId: regularCustomerOrders.productId, - * quantityOrdered: regularCustomerOrders.quantityOrdered - * }) - * .from(regularCustomerOrders) - * .intersectAll( - * db.select({ - * productId: vipCustomerOrders.productId, - * quantityOrdered: vipCustomerOrders.quantityOrdered - * }) - * .from(vipCustomerOrders) - * ); - * ``` - */ -export const intersectAll = createSetOperator('intersect', true); - /** * Adds `except` set operator to the query. * @@ -1183,48 +1055,6 @@ export const intersectAll = createSetOperator('intersect', true); */ export const except = createSetOperator('except', false); -/** - * Adds `except all` set operator to the query. - * - * Calling this method will retrieve all rows from the left query, except for the rows that are present in the result set of the right query. - * - * See docs: {@link https://orm.drizzle.team/docs/set-operations#except-all} - * - * @example - * - * ```ts - * // Select all products that are ordered by regular customers but not by VIP customers - * import { exceptAll } from 'drizzle-orm/mysql-core' - * - * await exceptAll( - * db.select({ - * productId: regularCustomerOrders.productId, - * quantityOrdered: regularCustomerOrders.quantityOrdered - * }) - * .from(regularCustomerOrders), - * db.select({ - * productId: vipCustomerOrders.productId, - * quantityOrdered: vipCustomerOrders.quantityOrdered - * }) - * .from(vipCustomerOrders) - * ); - * // or - * await db.select({ - * productId: regularCustomerOrders.productId, - * quantityOrdered: regularCustomerOrders.quantityOrdered, - * }) - * .from(regularCustomerOrders) - * .exceptAll( - * db.select({ - * productId: vipCustomerOrders.productId, - * quantityOrdered: vipCustomerOrders.quantityOrdered, - * }) - * .from(vipCustomerOrders) - * ); - * ``` - */ -export const exceptAll = createSetOperator('except', true); - /** * Adds `minus` set operator to the query. * diff --git a/drizzle-orm/src/singlestore-core/session.ts b/drizzle-orm/src/singlestore-core/session.ts index b95589a45..96158c50f 100644 --- a/drizzle-orm/src/singlestore-core/session.ts +++ b/drizzle-orm/src/singlestore-core/session.ts @@ -7,8 +7,6 @@ import { SingleStoreDatabase } from './db.ts'; import type { SingleStoreDialect } from './dialect.ts'; import type { SelectedFieldsOrdered } from './query-builders/select.types.ts'; -export type Mode = 'default' | 'planetscale'; - export interface SingleStoreQueryResultHKT { readonly $brand: 'SingleStoreQueryResultHKT'; readonly row: unknown; @@ -53,10 +51,10 @@ export abstract class SingleStorePreparedQuery): AsyncGenerator; } +// In SingleStore, there is no need to explicitly set the isolation level, since READ COMMITTED is the only isolation level supported and is set by default for all transactions export interface SingleStoreTransactionConfig { withConsistentSnapshot?: boolean; accessMode?: 'read only' | 'read write'; - isolationLevel: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'; } export abstract class SingleStoreSession< @@ -94,16 +92,6 @@ export abstract class SingleStoreSession< config?: SingleStoreTransactionConfig, ): Promise; - protected getSetTransactionSQL(config: SingleStoreTransactionConfig): SQL | undefined { - const parts: string[] = []; - - if (config.isolationLevel) { - parts.push(`isolation level ${config.isolationLevel}`); - } - - return parts.length ? sql.join(['set transaction ', parts.join(' ')]) : undefined; - } - protected getStartTransactionSQL(config: SingleStoreTransactionConfig): SQL | undefined { const parts: string[] = []; @@ -132,9 +120,8 @@ export abstract class SingleStoreTransaction< session: SingleStoreSession, protected schema: RelationalSchemaConfig | undefined, protected readonly nestedIndex: number, - mode: Mode, ) { - super(dialect, session, schema, mode); + super(dialect, session, schema); } rollback(): never { diff --git a/drizzle-orm/src/singlestore-proxy/driver.ts b/drizzle-orm/src/singlestore-proxy/driver.ts index f61040c9f..f54180c66 100644 --- a/drizzle-orm/src/singlestore-proxy/driver.ts +++ b/drizzle-orm/src/singlestore-proxy/driver.ts @@ -50,5 +50,5 @@ export function drizzle = Record; + return new SingleStoreDatabase(dialect, session, schema) as SingleStoreRemoteDatabase; } diff --git a/drizzle-orm/src/singlestore/driver.ts b/drizzle-orm/src/singlestore/driver.ts index 0cda87593..ffc5c2795 100644 --- a/drizzle-orm/src/singlestore/driver.ts +++ b/drizzle-orm/src/singlestore/driver.ts @@ -10,21 +10,24 @@ import { } from '~/relations.ts'; import { SingleStoreDatabase } from '~/singlestore-core/db.ts'; import { SingleStoreDialect } from '~/singlestore-core/dialect.ts'; -import type { Mode } from '~/singlestore-core/session.ts'; import type { DrizzleConfig } from '~/utils.ts'; -import { DrizzleError } from '../index.ts'; -import type { SingleStore2Client, SingleStore2PreparedQueryHKT, SingleStore2QueryResultHKT } from './session.ts'; -import { SingleStore2Session } from './session.ts'; +import type { + Mode, + SingleStoreDriverClient, + SingleStoreDriverPreparedQueryHKT, + SingleStoreDriverQueryResultHKT, +} from './session.ts'; +import { SingleStoreDriverSession } from './session.ts'; export interface SingleStoreDriverOptions { logger?: Logger; } -export class SingleStore2Driver { - static readonly [entityKind]: string = 'SingleStore2Driver'; +export class SingleStoreDriver { + static readonly [entityKind]: string = 'SingleStoreDriver'; constructor( - private client: SingleStore2Client, + private client: SingleStoreDriverClient, private dialect: SingleStoreDialect, private options: SingleStoreDriverOptions = {}, ) { @@ -32,26 +35,25 @@ export class SingleStore2Driver { createSession( schema: RelationalSchemaConfig | undefined, - mode: Mode, - ): SingleStore2Session, TablesRelationalConfig> { - return new SingleStore2Session(this.client, this.dialect, schema, { logger: this.options.logger, mode }); + ): SingleStoreDriverSession, TablesRelationalConfig> { + return new SingleStoreDriverSession(this.client, this.dialect, schema, { logger: this.options.logger }); } } export { SingleStoreDatabase } from '~/singlestore-core/db.ts'; -export type SingleStore2Database< +export type SingleStoreDriverDatabase< TSchema extends Record = Record, -> = SingleStoreDatabase; +> = SingleStoreDatabase; -export type SingleStore2DrizzleConfig = Record> = +export type SingleStoreDriverDrizzleConfig = Record> = & Omit, 'schema'> & ({ schema: TSchema; mode: Mode } | { schema?: undefined; mode?: Mode }); export function drizzle = Record>( - client: SingleStore2Client | CallbackConnection | CallbackPool, - config: SingleStore2DrizzleConfig = {}, -): SingleStore2Database { + client: SingleStoreDriverClient | CallbackConnection | CallbackPool, + config: DrizzleConfig = {}, +): SingleStoreDriverDatabase { const dialect = new SingleStoreDialect(); let logger; if (config.logger === true) { @@ -65,13 +67,6 @@ export function drizzle = Record | undefined; if (config.schema) { - if (config.mode === undefined) { - throw new DrizzleError({ - message: - 'You need to specify "mode": "planetscale" or "default" when providing a schema. Read more: https://orm.drizzle.team/docs/rqb#modes', - }); - } - const tablesConfig = extractTablesRelationalConfig( config.schema, createTableRelationsHelpers, @@ -83,15 +78,13 @@ export function drizzle = Record; + const driver = new SingleStoreDriver(client as SingleStoreDriverClient, dialect, { logger }); + const session = driver.createSession(schema); + return new SingleStoreDatabase(dialect, session, schema) as SingleStoreDriverDatabase; } interface CallbackClient { - promise(): SingleStore2Client; + promise(): SingleStoreDriverClient; } function isCallbackClient(client: any): client is CallbackClient { diff --git a/drizzle-orm/src/singlestore/migrator.ts b/drizzle-orm/src/singlestore/migrator.ts index 600b4dd10..6f342c0c5 100644 --- a/drizzle-orm/src/singlestore/migrator.ts +++ b/drizzle-orm/src/singlestore/migrator.ts @@ -1,9 +1,9 @@ import type { MigrationConfig } from '~/migrator.ts'; import { readMigrationFiles } from '~/migrator.ts'; -import type { SingleStore2Database } from './driver.ts'; +import type { SingleStoreDriverDatabase } from './driver.ts'; export async function migrate>( - db: SingleStore2Database, + db: SingleStoreDriverDatabase, config: MigrationConfig, ) { const migrations = readMigrationFiles(config); diff --git a/drizzle-orm/src/singlestore/session.ts b/drizzle-orm/src/singlestore/session.ts index 7a4be5257..0ea143971 100644 --- a/drizzle-orm/src/singlestore/session.ts +++ b/drizzle-orm/src/singlestore/session.ts @@ -18,7 +18,6 @@ import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations import type { SingleStoreDialect } from '~/singlestore-core/dialect.ts'; import type { SelectedFieldsOrdered } from '~/singlestore-core/query-builders/select.types.ts'; import { - type Mode, type PreparedQueryKind, SingleStorePreparedQuery, type SingleStorePreparedQueryConfig, @@ -32,7 +31,10 @@ import { fillPlaceholders, sql } from '~/sql/sql.ts'; import type { Query, SQL } from '~/sql/sql.ts'; import { type Assume, mapResultRow } from '~/utils.ts'; -export type SingleStore2Client = Pool | Connection; +// must keep this type here for compatibility with DrizzleConfig +export type Mode = 'default'; + +export type SingleStoreDriverClient = Pool | Connection; export type SingleStoreRawQueryResult = [ResultSetHeader, FieldPacket[]]; export type SingleStoreQueryResultType = RowDataPacket[][] | RowDataPacket[] | OkPacket | OkPacket[] | ResultSetHeader; @@ -40,14 +42,16 @@ export type SingleStoreQueryResult< T = any, > = [T extends ResultSetHeader ? T : T[], FieldPacket[]]; -export class SingleStore2PreparedQuery extends SingleStorePreparedQuery { - static readonly [entityKind]: string = 'SingleStore2PreparedQuery'; +export class SingleStoreDriverPreparedQuery + extends SingleStorePreparedQuery +{ + static readonly [entityKind]: string = 'SingleStoreDriverPreparedQuery'; private rawQuery: QueryOptions; private query: QueryOptions; constructor( - private client: SingleStore2Client, + private client: SingleStoreDriverClient, queryString: string, private params: unknown[], private logger: Logger, @@ -181,29 +185,26 @@ export class SingleStore2PreparedQuery } } -export interface SingleStore2SessionOptions { +export interface SingleStoreDriverSessionOptions { logger?: Logger; - mode: Mode; } -export class SingleStore2Session< +export class SingleStoreDriverSession< TFullSchema extends Record, TSchema extends TablesRelationalConfig, -> extends SingleStoreSession { - static readonly [entityKind]: string = 'SingleStore2Session'; +> extends SingleStoreSession { + static readonly [entityKind]: string = 'SingleStoreDriverSession'; private logger: Logger; - private mode: Mode; constructor( - private client: SingleStore2Client, + private client: SingleStoreDriverClient, dialect: SingleStoreDialect, private schema: RelationalSchemaConfig | undefined, - private options: SingleStore2SessionOptions, + private options: SingleStoreDriverSessionOptions, ) { super(dialect); this.logger = options.logger ?? new NoopLogger(); - this.mode = options.mode; } prepareQuery( @@ -212,10 +213,10 @@ export class SingleStore2Session< customResultMapper?: (rows: unknown[][]) => T['execute'], generatedIds?: Record[], returningIds?: SelectedFieldsOrdered, - ): PreparedQueryKind { + ): PreparedQueryKind { // Add returningId fields // Each driver gets them from response from database - return new SingleStore2PreparedQuery( + return new SingleStoreDriverPreparedQuery( this.client, query.sql, query.params, @@ -224,7 +225,7 @@ export class SingleStore2Session< customResultMapper, generatedIds, returningIds, - ) as PreparedQueryKind; + ) as PreparedQueryKind; } /** @@ -254,29 +255,24 @@ export class SingleStore2Session< } override async transaction( - transaction: (tx: SingleStore2Transaction) => Promise, + transaction: (tx: SingleStoreDriverTransaction) => Promise, config?: SingleStoreTransactionConfig, ): Promise { const session = isPool(this.client) - ? new SingleStore2Session( + ? new SingleStoreDriverSession( await this.client.getConnection(), this.dialect, this.schema, this.options, ) : this; - const tx = new SingleStore2Transaction( + const tx = new SingleStoreDriverTransaction( this.dialect, session as SingleStoreSession, this.schema, 0, - this.mode, ); if (config) { - const setTransactionConfigSql = this.getSetTransactionSQL(config); - if (setTransactionConfigSql) { - await tx.execute(setTransactionConfigSql); - } const startTransactionSql = this.getStartTransactionSQL(config); await (startTransactionSql ? tx.execute(startTransactionSql) : tx.execute(sql`begin`)); } else { @@ -297,22 +293,26 @@ export class SingleStore2Session< } } -export class SingleStore2Transaction< +export class SingleStoreDriverTransaction< TFullSchema extends Record, TSchema extends TablesRelationalConfig, -> extends SingleStoreTransaction { - static readonly [entityKind]: string = 'SingleStore2Transaction'; +> extends SingleStoreTransaction< + SingleStoreDriverQueryResultHKT, + SingleStoreDriverPreparedQueryHKT, + TFullSchema, + TSchema +> { + static readonly [entityKind]: string = 'SingleStoreDriverTransaction'; override async transaction( - transaction: (tx: SingleStore2Transaction) => Promise, + transaction: (tx: SingleStoreDriverTransaction) => Promise, ): Promise { const savepointName = `sp${this.nestedIndex + 1}`; - const tx = new SingleStore2Transaction( + const tx = new SingleStoreDriverTransaction( this.dialect, this.session, this.schema, this.nestedIndex + 1, - this.mode, ); await tx.execute(sql.raw(`savepoint ${savepointName}`)); try { @@ -326,14 +326,14 @@ export class SingleStore2Transaction< } } -function isPool(client: SingleStore2Client): client is Pool { +function isPool(client: SingleStoreDriverClient): client is Pool { return 'getConnection' in client; } -export interface SingleStore2QueryResultHKT extends SingleStoreQueryResultHKT { +export interface SingleStoreDriverQueryResultHKT extends SingleStoreQueryResultHKT { type: SingleStoreRawQueryResult; } -export interface SingleStore2PreparedQueryHKT extends SingleStorePreparedQueryHKT { - type: SingleStore2PreparedQuery>; +export interface SingleStoreDriverPreparedQueryHKT extends SingleStorePreparedQueryHKT { + type: SingleStoreDriverPreparedQuery>; } diff --git a/drizzle-orm/type-tests/singlestore/db.ts b/drizzle-orm/type-tests/singlestore/db.ts index bde149b08..f9bc6ff5f 100644 --- a/drizzle-orm/type-tests/singlestore/db.ts +++ b/drizzle-orm/type-tests/singlestore/db.ts @@ -7,8 +7,6 @@ export const db = drizzle(pool); { drizzle(pool); - // @ts-expect-error - missing mode + drizzle(pool, {}); drizzle(pool, { schema: {} }); - drizzle(pool, { schema: {}, mode: 'default' }); - drizzle(pool, { mode: 'default' }); } diff --git a/drizzle-orm/type-tests/singlestore/generated-columns.ts b/drizzle-orm/type-tests/singlestore/generated-columns.ts index e33a9d61b..e5b17a9b1 100644 --- a/drizzle-orm/type-tests/singlestore/generated-columns.ts +++ b/drizzle-orm/type-tests/singlestore/generated-columns.ts @@ -98,7 +98,7 @@ const users = singlestoreTable( } { - const db = drizzle({} as any, { schema: { users }, mode: 'default' }); + const db = drizzle({} as any, { schema: { users } }); const dbUser = await db.query.users.findFirst(); @@ -118,7 +118,7 @@ const users = singlestoreTable( } { - const db = drizzle({} as any, { schema: { users }, mode: 'default' }); + const db = drizzle({} as any, { schema: { users } }); const dbUser = await db.query.users.findMany(); diff --git a/drizzle-orm/type-tests/singlestore/set-operators.ts b/drizzle-orm/type-tests/singlestore/set-operators.ts index 8bd434262..aa4f21b9c 100644 --- a/drizzle-orm/type-tests/singlestore/set-operators.ts +++ b/drizzle-orm/type-tests/singlestore/set-operators.ts @@ -1,15 +1,7 @@ import { type Equal, Expect } from 'type-tests/utils.ts'; import { eq } from '~/expressions.ts'; -import { - except, - exceptAll, - intersect, - intersectAll, - type SingleStoreSetOperator, - union, - unionAll, -} from '~/singlestore-core/index.ts'; -import { desc, sql } from '~/sql/index.ts'; +import { except, intersect, type SingleStoreSetOperator, union, unionAll } from '~/singlestore-core/index.ts'; +import { sql } from '~/sql/index.ts'; import { db } from './db.ts'; import { cities, classes, newYorkers, users } from './tables.ts'; @@ -51,18 +43,6 @@ const intersectTest = await db Expect>; -const intersectAllTest = await db - .select({ id: users.id, homeCity: users.class }) - .from(users) - .intersect( - db - .select({ id: users.id, homeCity: users.class }) - .from(users) - .leftJoin(cities, eq(users.id, cities.id)), - ); - -Expect>; - const exceptTest = await db .select({ id: users.id, homeCity: users.homeCity }) .from(users) @@ -74,17 +54,6 @@ const exceptTest = await db Expect>; -const exceptAllTest = await db - .select({ id: users.id, homeCity: users.class }) - .from(users) - .except( - db - .select({ id: users.id, homeCity: sql<'A' | 'C'>`${users.class}` }) - .from(users), - ); - -Expect>; - const union2Test = await union(db.select().from(cities), db.select().from(cities), db.select().from(cities)); Expect>; @@ -120,24 +89,6 @@ const intersect2Test = await intersect( Expect>; -const intersectAll2Test = await intersectAll( - union( - db.select({ - id: cities.id, - }).from(cities), - db.select({ - id: cities.id, - }) - .from(cities).where(sql``), - ), - db.select({ - id: cities.id, - }) - .from(cities), -).orderBy(desc(cities.id)).limit(23); - -Expect>; - const except2Test = await except( db.select({ userId: newYorkers.userId, @@ -150,20 +101,6 @@ const except2Test = await except( Expect>; -const exceptAll2Test = await exceptAll( - db.select({ - userId: newYorkers.userId, - cityId: newYorkers.cityId, - }) - .from(newYorkers).where(sql``), - db.select({ - userId: newYorkers.userId, - cityId: newYorkers.cityId, - }).from(newYorkers).leftJoin(users, sql``), -); - -Expect>; - const unionfull = await union(db.select().from(users), db.select().from(users)).orderBy(sql``).limit(1).offset(2); Expect< diff --git a/integration-tests/.env.example b/integration-tests/.env.example index 50b3c95f2..70952e4f7 100644 --- a/integration-tests/.env.example +++ b/integration-tests/.env.example @@ -1,5 +1,6 @@ PG_CONNECTION_STRING="postgres://postgres:postgres@localhost:55432/postgres" MYSQL_CONNECTION_STRING="mysql://root:mysql@127.0.0.1:33306/drizzle" +SINGLESTORE_CONNECTION_STRING="singlestore://root:singlestore@localhost:3306/drizzle" PLANETSCALE_CONNECTION_STRING= TIDB_CONNECTION_STRING= NEON_CONNECTION_STRING= diff --git a/integration-tests/drizzle2/singlestore/0000_nostalgic_carnage.sql b/integration-tests/drizzle2/singlestore/0000_nostalgic_carnage.sql new file mode 100644 index 000000000..50efe47da --- /dev/null +++ b/integration-tests/drizzle2/singlestore/0000_nostalgic_carnage.sql @@ -0,0 +1,20 @@ +CREATE TABLE `cities_migration` ( + `id` int, + `fullname_name` text, + `state` text +); +--> statement-breakpoint +CREATE TABLE `users_migration` ( + `id` int PRIMARY KEY NOT NULL, + `full_name` text, + `phone` int, + `invited_by` int, + `city_id` int, + `date` timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE `users12` ( + `id` serial AUTO_INCREMENT PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL +); diff --git a/integration-tests/drizzle2/singlestore/meta/0000_snapshot.json b/integration-tests/drizzle2/singlestore/meta/0000_snapshot.json new file mode 100644 index 000000000..63d5ad187 --- /dev/null +++ b/integration-tests/drizzle2/singlestore/meta/0000_snapshot.json @@ -0,0 +1,132 @@ +{ + "version": "1", + "dialect": "singlestore", + "id": "8e8c8378-0496-40f6-88e3-98aab8282b1f", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "cities_migration": { + "name": "cities_migration", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fullname_name": { + "name": "fullname_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "users_migration": { + "name": "users_migration", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "city_id": { + "name": "city_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "users12": { + "name": "users12", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "my_unique_index": { + "name": "my_unique_index", + "columns": [ + "name" + ], + "isUnique": true, + "using": "btree" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/integration-tests/drizzle2/singlestore/meta/_journal.json b/integration-tests/drizzle2/singlestore/meta/_journal.json new file mode 100644 index 000000000..49e74f169 --- /dev/null +++ b/integration-tests/drizzle2/singlestore/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "dialect": "singlestore", + "entries": [ + { + "idx": 0, + "version": "1", + "when": 1680270921944, + "tag": "0000_nostalgic_carnage", + "breakpoints": true + } + ] +} diff --git a/integration-tests/tests/relational/singlestore.schema.ts b/integration-tests/tests/relational/singlestore.schema.ts new file mode 100644 index 000000000..ca3386ba0 --- /dev/null +++ b/integration-tests/tests/relational/singlestore.schema.ts @@ -0,0 +1,106 @@ +import { bigint, boolean, primaryKey, serial, singlestoreTable, text, timestamp } from 'drizzle-orm/singlestore-core'; + +import { relations } from 'drizzle-orm'; + +export const usersTable = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + verified: boolean('verified').notNull().default(false), + invitedBy: bigint('invited_by', { mode: 'number' }), +}); +export const usersConfig = relations(usersTable, ({ one, many }) => ({ + invitee: one(usersTable, { + fields: [usersTable.invitedBy], + references: [usersTable.id], + }), + usersToGroups: many(usersToGroupsTable), + posts: many(postsTable), + comments: many(commentsTable), +})); + +export const groupsTable = singlestoreTable('groups', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + description: text('description'), +}); +export const groupsConfig = relations(groupsTable, ({ many }) => ({ + usersToGroups: many(usersToGroupsTable), +})); + +export const usersToGroupsTable = singlestoreTable( + 'users_to_groups', + { + id: serial('id').primaryKey(), + userId: bigint('user_id', { mode: 'number' }).notNull(), + groupId: bigint('group_id', { mode: 'number' }).notNull(), + }, + (t) => ({ + pk: primaryKey(t.userId, t.groupId), + }), +); +export const usersToGroupsConfig = relations(usersToGroupsTable, ({ one }) => ({ + group: one(groupsTable, { + fields: [usersToGroupsTable.groupId], + references: [groupsTable.id], + }), + user: one(usersTable, { + fields: [usersToGroupsTable.userId], + references: [usersTable.id], + }), +})); + +export const postsTable = singlestoreTable('posts', { + id: serial('id').primaryKey(), + content: text('content').notNull(), + ownerId: bigint('owner_id', { mode: 'number' }), + createdAt: timestamp('created_at') + .notNull() + .defaultNow(), +}); +export const postsConfig = relations(postsTable, ({ one, many }) => ({ + author: one(usersTable, { + fields: [postsTable.ownerId], + references: [usersTable.id], + }), + comments: many(commentsTable), +})); + +export const commentsTable = singlestoreTable('comments', { + id: serial('id').primaryKey(), + content: text('content').notNull(), + creator: bigint('creator', { mode: 'number' }), + postId: bigint('post_id', { mode: 'number' }), + createdAt: timestamp('created_at') + .notNull() + .defaultNow(), +}); +export const commentsConfig = relations(commentsTable, ({ one, many }) => ({ + post: one(postsTable, { + fields: [commentsTable.postId], + references: [postsTable.id], + }), + author: one(usersTable, { + fields: [commentsTable.creator], + references: [usersTable.id], + }), + likes: many(commentLikesTable), +})); + +export const commentLikesTable = singlestoreTable('comment_likes', { + id: serial('id').primaryKey(), + creator: bigint('creator', { mode: 'number' }), + commentId: bigint('comment_id', { mode: 'number' }), + createdAt: timestamp('created_at') + .notNull() + .defaultNow(), +}); +export const commentLikesConfig = relations(commentLikesTable, ({ one }) => ({ + comment: one(commentsTable, { + fields: [commentLikesTable.commentId], + references: [commentsTable.id], + }), + author: one(usersTable, { + fields: [commentLikesTable.creator], + references: [usersTable.id], + }), +})); diff --git a/integration-tests/tests/relational/singlestore.test.ts b/integration-tests/tests/relational/singlestore.test.ts new file mode 100644 index 000000000..50aa2e8f4 --- /dev/null +++ b/integration-tests/tests/relational/singlestore.test.ts @@ -0,0 +1,6402 @@ +import retry from 'async-retry'; +import Docker from 'dockerode'; +import 'dotenv/config'; +import { desc, DrizzleError, eq, gt, gte, or, placeholder, sql, TransactionRollbackError } from 'drizzle-orm'; +import { drizzle, type SingleStoreDriverDatabase } from 'drizzle-orm/singlestore'; +import getPort from 'get-port'; +import * as mysql from 'mysql2/promise'; +import { v4 as uuid } from 'uuid'; +import { afterAll, beforeAll, beforeEach, expect, expectTypeOf, test } from 'vitest'; +import * as schema from './singlestore.schema.ts'; + +const { usersTable, postsTable, commentsTable, usersToGroupsTable, groupsTable } = schema; + +const ENABLE_LOGGING = false; + +/* + Test cases: + - querying nested relation without PK with additional fields +*/ + +declare module 'vitest' { + export interface TestContext { + docker: Docker; + singlestoreContainer: Docker.Container; + singlestoreDb: SingleStoreDriverDatabase; + singlestoreClient: mysql.Connection; + } +} + +let globalDocker: Docker; +let singlestoreContainer: Docker.Container; +let db: SingleStoreDriverDatabase; +let client: mysql.Connection; + +async function createDockerDB(): Promise { + const docker = new Docker(); + const port = await getPort({ port: 3306 }); + const image = 'ghcr.io/singlestore-labs/singlestoredb-dev:latest'; + + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve(err))) + ); + + singlestoreContainer = await docker.createContainer({ + Image: image, + Env: ['ROOT_PASSWORD=singlestore'], + name: `drizzle-integration-tests-${uuid()}`, + HostConfig: { + AutoRemove: true, + PortBindings: { + '3306/tcp': [{ HostPort: `${port}` }], + }, + }, + }); + + await singlestoreContainer.start(); + await new Promise((resolve) => setTimeout(resolve, 4000)); + + return `singlestore://root:singlestore@localhost:${port}/`; +} + +beforeAll(async () => { + const connectionString = process.env['SINGLESTORE_CONNECTION_STRING'] ?? (await createDockerDB()); + client = await retry(async () => { + client = await mysql.createConnection(connectionString); + await client.connect(); + return client; + }, { + retries: 20, + factor: 1, + minTimeout: 250, + maxTimeout: 250, + randomize: false, + onRetry() { + client?.end(); + }, + }); + + await client.query(`CREATE DATABASE IF NOT EXISTS drizzle;`); + await client.changeUser({ database: 'drizzle' }); + db = drizzle(client, { schema, logger: ENABLE_LOGGING }); +}); + +afterAll(async () => { + await client?.end().catch(console.error); + await singlestoreContainer?.stop().catch(console.error); +}); + +beforeEach(async (ctx) => { + ctx.singlestoreDb = db; + ctx.singlestoreClient = client; + ctx.docker = globalDocker; + ctx.singlestoreContainer = singlestoreContainer; + + await ctx.singlestoreDb.execute(sql`drop table if exists \`users\``); + await ctx.singlestoreDb.execute(sql`drop table if exists \`groups\``); + await ctx.singlestoreDb.execute(sql`drop table if exists \`users_to_groups\``); + await ctx.singlestoreDb.execute(sql`drop table if exists \`posts\``); + await ctx.singlestoreDb.execute(sql`drop table if exists \`comments\``); + await ctx.singlestoreDb.execute(sql`drop table if exists \`comment_likes\``); + + await ctx.singlestoreDb.execute( + sql` + CREATE TABLE \`users\` ( + \`id\` serial PRIMARY KEY NOT NULL, + \`name\` text NOT NULL, + \`verified\` boolean DEFAULT false NOT NULL, + \`invited_by\` bigint + ); + `, + ); + await ctx.singlestoreDb.execute( + sql` + CREATE TABLE \`groups\` ( + \`id\` serial PRIMARY KEY NOT NULL, + \`name\` text NOT NULL, + \`description\` text + ); + `, + ); + await ctx.singlestoreDb.execute( + sql` + CREATE TABLE \`users_to_groups\` ( + \`id\` serial PRIMARY KEY NOT NULL, + \`user_id\` bigint, + \`group_id\` bigint + ); + `, + ); + await ctx.singlestoreDb.execute( + sql` + CREATE TABLE \`posts\` ( + \`id\` serial PRIMARY KEY NOT NULL, + \`content\` text NOT NULL, + \`owner_id\` bigint, + \`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + `, + ); + await ctx.singlestoreDb.execute( + sql` + CREATE TABLE \`comments\` ( + \`id\` serial PRIMARY KEY NOT NULL, + \`content\` text NOT NULL, + \`creator\` bigint, + \`post_id\` bigint, + \`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + `, + ); + await ctx.singlestoreDb.execute( + sql` + CREATE TABLE \`comment_likes\` ( + \`id\` serial PRIMARY KEY NOT NULL, + \`creator\` bigint, + \`comment_id\` bigint, + \`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + `, + ); +}); + +/* + [Find Many] One relation users+posts +*/ + +test('[Find Many] Get users with posts', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + with: { + posts: true, + }, + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + usersWithPosts.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(usersWithPosts.length).eq(3); + expect(usersWithPosts[0]?.posts.length).eq(1); + expect(usersWithPosts[1]?.posts.length).eq(1); + expect(usersWithPosts[2]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + posts: [{ id: 2, ownerId: 2, content: 'Post2', createdAt: usersWithPosts[1]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts[2]).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + posts: [{ id: 3, ownerId: 3, content: 'Post3', createdAt: usersWithPosts[2]?.posts[0]?.createdAt }], + }); +}); + +test.skip('[Find Many] Get users with posts + limit posts', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + with: { + posts: { + limit: 1, + }, + }, + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + usersWithPosts.sort((a, b) => (a.id > b.id) ? 1 : -1); + usersWithPosts[0]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + usersWithPosts[1]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + usersWithPosts[2]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(usersWithPosts.length).eq(3); + expect(usersWithPosts[0]?.posts.length).eq(1); + expect(usersWithPosts[1]?.posts.length).eq(1); + expect(usersWithPosts[2]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + posts: [{ id: 4, ownerId: 2, content: 'Post2', createdAt: usersWithPosts[1]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts[2]).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + posts: [{ id: 6, ownerId: 3, content: 'Post3', createdAt: usersWithPosts[2]?.posts[0]?.createdAt }], + }); +}); + +test.skip('[Find Many] Get users with posts + limit posts and users', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + limit: 2, + with: { + posts: { + limit: 1, + }, + }, + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + usersWithPosts.sort((a, b) => (a.id > b.id) ? 1 : -1); + usersWithPosts[0]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + usersWithPosts[1]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(usersWithPosts.length).eq(2); + expect(usersWithPosts[0]?.posts.length).eq(1); + expect(usersWithPosts[1]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + posts: [{ id: 4, ownerId: 2, content: 'Post2', createdAt: usersWithPosts[1]?.posts[0]?.createdAt }], + }); +}); + +test('[Find Many] Get users with posts + custom fields', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + with: { + posts: true, + }, + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + lowerName: string; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + usersWithPosts.sort((a, b) => (a.id > b.id) ? 1 : -1); + usersWithPosts[0]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + usersWithPosts[1]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + usersWithPosts[2]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(usersWithPosts.length).toEqual(3); + expect(usersWithPosts[0]?.posts.length).toEqual(3); + expect(usersWithPosts[1]?.posts.length).toEqual(2); + expect(usersWithPosts[2]?.posts.length).toEqual(2); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + lowerName: 'dan', + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }, { + id: 2, + ownerId: 1, + content: 'Post1.2', + createdAt: usersWithPosts[0]?.posts[1]?.createdAt, + }, { id: 3, ownerId: 1, content: 'Post1.3', createdAt: usersWithPosts[0]?.posts[2]?.createdAt }], + }); + expect(usersWithPosts[1]).toEqual({ + id: 2, + name: 'Andrew', + lowerName: 'andrew', + verified: false, + invitedBy: null, + posts: [{ id: 4, ownerId: 2, content: 'Post2', createdAt: usersWithPosts[1]?.posts[0]?.createdAt }, { + id: 5, + ownerId: 2, + content: 'Post2.1', + createdAt: usersWithPosts[1]?.posts[1]?.createdAt, + }], + }); + expect(usersWithPosts[2]).toEqual({ + id: 3, + name: 'Alex', + lowerName: 'alex', + verified: false, + invitedBy: null, + posts: [{ id: 6, ownerId: 3, content: 'Post3', createdAt: usersWithPosts[2]?.posts[0]?.createdAt }, { + id: 7, + ownerId: 3, + content: 'Post3.1', + createdAt: usersWithPosts[2]?.posts[1]?.createdAt, + }], + }); +}); + +test.skip('[Find Many] Get users with posts + custom fields + limits', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + limit: 1, + with: { + posts: { + limit: 1, + }, + }, + extras: (usersTable, { sql }) => ({ + lowerName: sql`lower(${usersTable.name})`.as('name_lower'), + }), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + lowerName: string; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).toEqual(1); + expect(usersWithPosts[0]?.posts.length).toEqual(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + lowerName: 'dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); +}); + +test.skip('[Find Many] Get users with posts + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: '1' }, + { ownerId: 1, content: '2' }, + { ownerId: 1, content: '3' }, + { ownerId: 2, content: '4' }, + { ownerId: 2, content: '5' }, + { ownerId: 3, content: '6' }, + { ownerId: 3, content: '7' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + with: { + posts: { + orderBy: (postsTable, { desc }) => [desc(postsTable.content)], + }, + }, + orderBy: (usersTable, { desc }) => [desc(usersTable.id)], + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(3); + expect(usersWithPosts[0]?.posts.length).eq(2); + expect(usersWithPosts[1]?.posts.length).eq(2); + expect(usersWithPosts[2]?.posts.length).eq(3); + + expect(usersWithPosts[2]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 3, ownerId: 1, content: '3', createdAt: usersWithPosts[2]?.posts[2]?.createdAt }, { + id: 2, + ownerId: 1, + content: '2', + createdAt: usersWithPosts[2]?.posts[1]?.createdAt, + }, { id: 1, ownerId: 1, content: '1', createdAt: usersWithPosts[2]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + posts: [{ + id: 5, + ownerId: 2, + content: '5', + createdAt: usersWithPosts[1]?.posts[1]?.createdAt, + }, { id: 4, ownerId: 2, content: '4', createdAt: usersWithPosts[1]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts[0]).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + posts: [{ + id: 7, + ownerId: 3, + content: '7', + createdAt: usersWithPosts[0]?.posts[1]?.createdAt, + }, { id: 6, ownerId: 3, content: '6', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); +}); + +test('[Find Many] Get users with posts + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + where: (({ id }, { eq }) => eq(id, 1)), + with: { + posts: { + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(1); + expect(usersWithPosts[0]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); +}); + +test('[Find Many] Get users with posts + where + partial', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + columns: { + id: true, + name: true, + }, + with: { + posts: { + columns: { + id: true, + content: true, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + posts: { + id: number; + content: string; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(1); + expect(usersWithPosts[0]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + posts: [{ id: 1, content: 'Post1' }], + }); +}); + +test('[Find Many] Get users with posts + where + partial. Did not select posts id, but used it in where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + columns: { + id: true, + name: true, + }, + with: { + posts: { + columns: { + id: true, + content: true, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + posts: { + id: number; + content: string; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(1); + expect(usersWithPosts[0]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + posts: [{ id: 1, content: 'Post1' }], + }); +}); + +test('[Find Many] Get users with posts + where + partial(true + false)', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + columns: { + id: true, + name: false, + }, + with: { + posts: { + columns: { + id: true, + content: false, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + posts: { + id: number; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(1); + expect(usersWithPosts[0]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + posts: [{ id: 1 }], + }); +}); + +test('[Find Many] Get users with posts + where + partial(false)', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + columns: { + name: false, + }, + with: { + posts: { + columns: { + content: false, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(1); + expect(usersWithPosts[0]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); +}); + +test('[Find Many] Get users with posts in transaction', async (t) => { + const { singlestoreDb: db } = t; + + let usersWithPosts: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[] = []; + + await db.transaction(async (tx) => { + await tx.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await tx.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + usersWithPosts = await tx.query.usersTable.findMany({ + where: (({ id }, { eq }) => eq(id, 1)), + with: { + posts: { + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + }); + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(1); + expect(usersWithPosts[0]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); +}); + +test('[Find Many] Get users with posts in rollbacked transaction', async (t) => { + const { singlestoreDb: db } = t; + + let usersWithPosts: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[] = []; + + await expect(db.transaction(async (tx) => { + await tx.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await tx.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + tx.rollback(); + + usersWithPosts = await tx.query.usersTable.findMany({ + where: (({ id }, { eq }) => eq(id, 1)), + with: { + posts: { + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + }); + })).rejects.toThrowError(new TransactionRollbackError()); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(0); +}); + +// select only custom +test('[Find Many] Get only custom fields', async () => { + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { id: 1, ownerId: 1, content: 'Post1' }, + { id: 2, ownerId: 1, content: 'Post1.2' }, + { id: 3, ownerId: 1, content: 'Post1.3' }, + { id: 4, ownerId: 2, content: 'Post2' }, + { id: 5, ownerId: 2, content: 'Post2.1' }, + { id: 6, ownerId: 3, content: 'Post3' }, + { id: 7, ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + columns: {}, + with: { + posts: { + columns: {}, + extras: ({ content }) => ({ + lowerName: sql`lower(${content})`.as('content_lower'), + }), + }, + }, + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + // Type Assertion + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + lowerName: string; + posts: { + lowerName: string; + }[]; + }[]>(); + + // General Assertions + expect(usersWithPosts).toHaveLength(3); + + // Helper function to find user by lowerName + const findUser = (lowerName: string) => usersWithPosts.find((user) => user.lowerName === lowerName); + + // Assertions for each user + const dan = findUser('dan'); + const andrew = findUser('andrew'); + const alex = findUser('alex'); + + expect(dan).toBeDefined(); + expect(andrew).toBeDefined(); + expect(alex).toBeDefined(); + + // Verify the number of posts for each user + expect(dan?.posts).toHaveLength(3); + expect(andrew?.posts).toHaveLength(2); + expect(alex?.posts).toHaveLength(2); + + // Define expected posts for each user + const expectedDanPosts = ['post1', 'post1.2', 'post1.3']; + const expectedAndrewPosts = ['post2', 'post2.1']; + const expectedAlexPosts = ['post3', 'post3.1']; + + // Helper function to extract lowerNames from posts + const getPostLowerNames = (posts: { lowerName: string }[]) => posts.map((post) => post.lowerName); + + // Assertions for Dan's posts + expect(getPostLowerNames(dan!.posts)).toEqual(expect.arrayContaining(expectedDanPosts)); + expect(getPostLowerNames(dan!.posts)).toHaveLength(expectedDanPosts.length); + + // Assertions for Andrew's posts + expect(getPostLowerNames(andrew!.posts)).toEqual(expect.arrayContaining(expectedAndrewPosts)); + expect(getPostLowerNames(andrew!.posts)).toHaveLength(expectedAndrewPosts.length); + + // Assertions for Alex's posts + expect(getPostLowerNames(alex!.posts)).toEqual(expect.arrayContaining(expectedAlexPosts)); + expect(getPostLowerNames(alex!.posts)).toHaveLength(expectedAlexPosts.length); +}); + +// select only custom with where clause (Order Agnostic) +test('[Find Many] Get only custom fields + where', async (t) => { + const { singlestoreDb: db } = t; + + // Insert Users + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + // Insert Posts + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + // Query Users with Posts where users.id = 1 and posts.id >= 2 + const usersWithPosts = await db.query.usersTable.findMany({ + columns: {}, + with: { + posts: { + columns: {}, + where: gte(postsTable.id, 2), + extras: ({ content }) => ({ + lowerName: sql`lower(${content})`.as('content_lower'), + }), + }, + }, + where: eq(usersTable.id, 1), + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + // Type Assertion + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + lowerName: string; + posts: { + lowerName: string; + }[]; + }[]>(); + + // General Assertions + expect(usersWithPosts).toHaveLength(1); + + // Since we expect only one user, we can extract it directly + const danWithPosts = usersWithPosts[0]; + + // Assert that the user exists and has the correct lowerName + expect(danWithPosts).toBeDefined(); + expect(danWithPosts?.lowerName).toBe('dan'); + + // Assert that the user has the expected number of posts + expect(danWithPosts?.posts).toHaveLength(2); + + // Define the expected posts + const expectedPosts = ['post1.2', 'post1.3']; + + // Extract the lowerName of each post + const actualPostLowerNames = danWithPosts?.posts.map((post) => post.lowerName); + + // Assert that all expected posts are present, regardless of order + for (const expectedPost of expectedPosts) { + expect(actualPostLowerNames).toContain(expectedPost); + } + + // Additionally, ensure no unexpected posts are present + expect(actualPostLowerNames).toHaveLength(expectedPosts.length); +}); + +test.skip('[Find Many] Get only custom fields + where + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + columns: {}, + with: { + posts: { + columns: {}, + where: gte(postsTable.id, 2), + limit: 1, + extras: ({ content }) => ({ + lowerName: sql`lower(${content})`.as('content_lower'), + }), + }, + }, + where: eq(usersTable.id, 1), + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + lowerName: string; + posts: { + lowerName: string; + }[]; + }[]>(); + + expect(usersWithPosts.length).toEqual(1); + expect(usersWithPosts[0]?.posts.length).toEqual(1); + + expect(usersWithPosts).toContainEqual({ + lowerName: 'dan', + posts: [{ lowerName: 'post1.2' }], + }); +}); + +test.skip('[Find Many] Get only custom fields + where + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findMany({ + columns: {}, + with: { + posts: { + columns: {}, + where: gte(postsTable.id, 2), + orderBy: [desc(postsTable.id)], + extras: ({ content }) => ({ + lowerName: sql`lower(${content})`.as('content_lower'), + }), + }, + }, + where: eq(usersTable.id, 1), + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + lowerName: string; + posts: { + lowerName: string; + }[]; + }[]>(); + + expect(usersWithPosts.length).toEqual(1); + expect(usersWithPosts[0]?.posts.length).toEqual(2); + + expect(usersWithPosts).toContainEqual({ + lowerName: 'dan', + posts: [{ lowerName: 'post1.3' }, { lowerName: 'post1.2' }], + }); +}); + +// select only custom find one (Order Agnostic) +test('[Find One] Get only custom fields (Order Agnostic)', async () => { + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + // Query to find the first user without any specific order + const usersWithPosts = await db.query.usersTable.findFirst({ + columns: {}, + with: { + posts: { + columns: {}, + extras: ({ content }) => ({ + lowerName: sql`lower(${content})`.as('content_lower'), + }), + }, + }, + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + // Type Assertion + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + lowerName: string; + posts: { + lowerName: string; + }[]; + } | undefined + >(); + + // General Assertions + expect(usersWithPosts).toBeDefined(); + + // Since findFirst without orderBy can return any user, we'll verify the returned user and their posts + if (usersWithPosts) { + // Define expected users and their corresponding posts + const expectedUsers: { [key: string]: string[] } = { + dan: ['post1', 'post1.2', 'post1.3'], + andrew: ['post2', 'post2.1'], + alex: ['post3', 'post3.1'], + }; + + // Verify that the returned user is one of the expected users + expect(Object.keys(expectedUsers)).toContain(usersWithPosts.lowerName); + + // Get the expected posts for the returned user + const expectedPosts = expectedUsers[usersWithPosts.lowerName] as string[]; + + // Verify the number of posts + expect(usersWithPosts.posts).toHaveLength(expectedPosts.length); + + // Extract the lowerName of each post + const actualPostLowerNames = usersWithPosts.posts.map((post) => post.lowerName); + + // Assert that all expected posts are present, regardless of order + for (const expectedPost of expectedPosts) { + expect(actualPostLowerNames).toContain(expectedPost.toLowerCase()); + } + } +}); + +// select only custom find one with where clause (Order Agnostic) +test('[Find One] Get only custom fields + where (Order Agnostic)', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + // Query to find the first user with id = 1 and posts with id >= 2 + const usersWithPosts = await db.query.usersTable.findFirst({ + columns: {}, + with: { + posts: { + columns: {}, + where: gte(postsTable.id, 2), + extras: ({ content }) => ({ + lowerName: sql`lower(${content})`.as('content_lower'), + }), + }, + }, + where: eq(usersTable.id, 1), + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + // Type Assertion + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + lowerName: string; + posts: { + lowerName: string; + }[]; + } | undefined + >(); + + // General Assertions + expect(usersWithPosts).toBeDefined(); + + if (usersWithPosts) { + // Assert that the returned user has the expected lowerName + expect(usersWithPosts.lowerName).toBe('dan'); + + // Assert that the user has exactly two posts + expect(usersWithPosts.posts).toHaveLength(2); + + // Define the expected posts + const expectedPosts = ['post1.2', 'post1.3']; + + // Extract the lowerName of each post + const actualPostLowerNames = usersWithPosts.posts.map((post) => post.lowerName); + + // Assert that all expected posts are present, regardless of order + for (const expectedPost of expectedPosts) { + expect(actualPostLowerNames).toContain(expectedPost.toLowerCase()); + } + + // Additionally, ensure no unexpected posts are present + expect(actualPostLowerNames).toHaveLength(expectedPosts.length); + } +}); + +test.skip('[Find One] Get only custom fields + where + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + columns: {}, + with: { + posts: { + columns: {}, + where: gte(postsTable.id, 2), + limit: 1, + extras: ({ content }) => ({ + lowerName: sql`lower(${content})`.as('content_lower'), + }), + }, + }, + where: eq(usersTable.id, 1), + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + lowerName: string; + posts: { + lowerName: string; + }[]; + } | undefined + >(); + + expect(usersWithPosts?.posts.length).toEqual(1); + + expect(usersWithPosts).toEqual({ + lowerName: 'dan', + posts: [{ lowerName: 'post1.2' }], + }); +}); + +test.skip('[Find One] Get only custom fields + where + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + columns: {}, + with: { + posts: { + columns: {}, + where: gte(postsTable.id, 2), + orderBy: [desc(postsTable.id)], + extras: ({ content }) => ({ + lowerName: sql`lower(${content})`.as('content_lower'), + }), + }, + }, + where: eq(usersTable.id, 1), + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + lowerName: string; + posts: { + lowerName: string; + }[]; + } | undefined + >(); + + expect(usersWithPosts?.posts.length).toEqual(2); + + expect(usersWithPosts).toEqual({ + lowerName: 'dan', + posts: [{ lowerName: 'post1.3' }, { lowerName: 'post1.2' }], + }); +}); + +// columns {} +test('[Find Many] Get select {}', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await expect( + async () => + await db.query.usersTable.findMany({ + columns: {}, + }), + ).rejects.toThrow(DrizzleError); +}); + +// columns {} +test('[Find One] Get select {}', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await expect(async () => + await db.query.usersTable.findFirst({ + columns: {}, + }) + ).rejects.toThrow(DrizzleError); +}); + +// deep select {} +test('[Find Many] Get deep select {}', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + await expect(async () => + await db.query.usersTable.findMany({ + columns: {}, + with: { + posts: { + columns: {}, + }, + }, + }) + ).rejects.toThrow(DrizzleError); +}); + +// deep select {} +test('[Find One] Get deep select {}', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + await expect(async () => + await db.query.usersTable.findFirst({ + columns: {}, + with: { + posts: { + columns: {}, + }, + }, + }) + ).rejects.toThrow(DrizzleError); +}); + +/* + Prepared statements for users+posts +*/ +test.skip('[Find Many] Get users with posts + prepared limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const prepared = db.query.usersTable.findMany({ + with: { + posts: { + limit: placeholder('limit'), + }, + }, + }).prepare(); + + const usersWithPosts = await prepared.execute({ limit: 1 }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(3); + expect(usersWithPosts[0]?.posts.length).eq(1); + expect(usersWithPosts[1]?.posts.length).eq(1); + expect(usersWithPosts[2]?.posts.length).eq(1); + + expect(usersWithPosts).toContainEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts).toContainEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + posts: [{ id: 4, ownerId: 2, content: 'Post2', createdAt: usersWithPosts[1]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + posts: [{ id: 6, ownerId: 3, content: 'Post3', createdAt: usersWithPosts[2]?.posts[0]?.createdAt }], + }); +}); + +test.skip('[Find Many] Get users with posts + prepared limit + offset', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const prepared = db.query.usersTable.findMany({ + limit: placeholder('uLimit'), + offset: placeholder('uOffset'), + with: { + posts: { + limit: placeholder('pLimit'), + }, + }, + }).prepare(); + + const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1 }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(2); + expect(usersWithPosts[0]?.posts.length).eq(1); + expect(usersWithPosts[1]?.posts.length).eq(1); + + expect(usersWithPosts).toContainEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + posts: [{ id: 4, ownerId: 2, content: 'Post2', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); + expect(usersWithPosts).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + posts: [{ id: 6, ownerId: 3, content: 'Post3', createdAt: usersWithPosts[1]?.posts[0]?.createdAt }], + }); +}); + +test('[Find Many] Get users with posts + prepared where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const prepared = db.query.usersTable.findMany({ + where: (({ id }, { eq }) => eq(id, placeholder('id'))), + with: { + posts: { + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + }).prepare(); + + const usersWithPosts = await prepared.execute({ id: 1 }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(1); + expect(usersWithPosts[0]?.posts.length).eq(1); + + expect(usersWithPosts[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); +}); + +test.skip('[Find Many] Get users with posts + prepared + limit + offset + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const prepared = db.query.usersTable.findMany({ + limit: placeholder('uLimit'), + offset: placeholder('uOffset'), + where: (({ id }, { eq, or }) => or(eq(id, placeholder('id')), eq(id, 3))), + with: { + posts: { + where: (({ id }, { eq }) => eq(id, placeholder('pid'))), + limit: placeholder('pLimit'), + }, + }, + }).prepare(); + + const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6 }); + + expectTypeOf(usersWithPosts).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + }[]>(); + + expect(usersWithPosts.length).eq(1); + expect(usersWithPosts[0]?.posts.length).eq(1); + + expect(usersWithPosts).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + posts: [{ id: 6, ownerId: 3, content: 'Post3', createdAt: usersWithPosts[0]?.posts[0]?.createdAt }], + }); +}); + +/* + [Find One] One relation users+posts +*/ + +test('[Find One] Get users with posts', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + with: { + posts: true, + }, + }); + + // Type Assertion + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + // General Assertions + expect(usersWithPosts).toBeDefined(); + + if (usersWithPosts) { + const { id, name, posts } = usersWithPosts; + + // Verify that the user is one of the inserted users + const validUsers: { [key: number]: string } = { + 1: 'dan', + 2: 'andrew', + 3: 'alex', + }; + expect(validUsers[id]).toBe(name.toLowerCase()); + + // Assert that the user has exactly one post + expect(posts).toHaveLength(1); + + const post = posts[0]; + + // Verify that the post belongs to the user + expect(post?.ownerId).toBe(id); + + // Verify that the post content matches the user + const expectedPostContent = `Post${id}`; + expect(post?.content.toLowerCase()).toBe(expectedPostContent.toLowerCase()); + + // Optionally, verify the presence of `createdAt` + expect(post?.createdAt).toBeInstanceOf(Date); + } +}); + +test.skip('[Find One] Get users with posts + limit posts', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + with: { + posts: { + limit: 1, + }, + }, + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).eq(1); + + expect(usersWithPosts).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts?.posts[0]?.createdAt }], + }); +}); + +test.skip('[Find One] Get users with posts no results found', async (t) => { + const { singlestoreDb: db } = t; + + const usersWithPosts = await db.query.usersTable.findFirst({ + with: { + posts: { + limit: 1, + }, + }, + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + expect(usersWithPosts).toBeUndefined(); +}); + +test.skip('[Find One] Get users with posts + limit posts and users', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + with: { + posts: { + limit: 1, + }, + }, + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).eq(1); + + expect(usersWithPosts).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts?.posts[0]?.createdAt }], + }); +}); + +test('[Find One] Get users with posts + custom fields', async () => { + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + with: { + posts: true, + }, + extras: ({ name }) => ({ + lowerName: sql`lower(${name})`.as('name_lower'), + }), + }); + + // Type Assertion + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + lowerName: string; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + // General Assertions + expect(usersWithPosts).toBeDefined(); + + if (usersWithPosts) { + const { id, lowerName, posts } = usersWithPosts; + + // Define valid users and their expected lower names + const validUsers: { [key: number]: string } = { + 1: 'dan', + 2: 'andrew', + 3: 'alex', + }; + + // Verify that the returned user's lowerName matches the expected value + expect(validUsers[id]).toBe(lowerName); + + // Define the expected posts based on the user ID + const expectedPostsByUser: Record = { + 1: ['post1', 'post1.2', 'post1.3'], + 2: ['post2', 'post2.1'], + 3: ['post3', 'post3.1'], + }; + + // Get the expected posts for the returned user + const expectedPosts = expectedPostsByUser[id] || []; + + // Extract the lowerName of each post + const actualPostContents = posts.map((post) => post.content.toLowerCase()); + + // Assert that all expected posts are present, regardless of order + for (const expectedPost of expectedPosts) { + expect(actualPostContents).toContain(expectedPost.toLowerCase()); + } + + // Optionally, ensure that no unexpected posts are present + expect(actualPostContents).toHaveLength(expectedPosts.length); + } +}); + +test.skip('[Find One] Get users with posts + custom fields + limits', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.2' }, + { ownerId: 1, content: 'Post1.3' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + with: { + posts: { + limit: 1, + }, + }, + extras: (usersTable, { sql }) => ({ + lowerName: sql`lower(${usersTable.name})`.as('name_lower'), + }), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + lowerName: string; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).toEqual(1); + + expect(usersWithPosts).toEqual({ + id: 1, + name: 'Dan', + lowerName: 'dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts?.posts[0]?.createdAt }], + }); +}); + +test.skip('[Find One] Get users with posts + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: '1' }, + { ownerId: 1, content: '2' }, + { ownerId: 1, content: '3' }, + { ownerId: 2, content: '4' }, + { ownerId: 2, content: '5' }, + { ownerId: 3, content: '6' }, + { ownerId: 3, content: '7' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + with: { + posts: { + orderBy: (postsTable, { desc }) => [desc(postsTable.content)], + }, + }, + orderBy: (usersTable, { desc }) => [desc(usersTable.id)], + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).eq(2); + + expect(usersWithPosts).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + posts: [{ + id: 7, + ownerId: 3, + content: '7', + createdAt: usersWithPosts?.posts[1]?.createdAt, + }, { id: 6, ownerId: 3, content: '6', createdAt: usersWithPosts?.posts[0]?.createdAt }], + }); +}); + +test('[Find One] Get users with posts + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + where: (({ id }, { eq }) => eq(id, 1)), + with: { + posts: { + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).eq(1); + + expect(usersWithPosts).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: usersWithPosts?.posts[0]?.createdAt }], + }); +}); + +test('[Find One] Get users with posts + where + partial', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + columns: { + id: true, + name: true, + }, + with: { + posts: { + columns: { + id: true, + content: true, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + posts: { + id: number; + content: string; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).eq(1); + + expect(usersWithPosts).toEqual({ + id: 1, + name: 'Dan', + posts: [{ id: 1, content: 'Post1' }], + }); +}); + +test('[Find One] Get users with posts + where + partial. Did not select posts id, but used it in where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + columns: { + id: true, + name: true, + }, + with: { + posts: { + columns: { + id: true, + content: true, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + name: string; + posts: { + id: number; + content: string; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).eq(1); + + expect(usersWithPosts).toEqual({ + id: 1, + name: 'Dan', + posts: [{ id: 1, content: 'Post1' }], + }); +}); + +test('[Find One] Get users with posts + where + partial(true + false)', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + columns: { + id: true, + name: false, + }, + with: { + posts: { + columns: { + id: true, + content: false, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + posts: { + id: number; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).eq(1); + + expect(usersWithPosts).toEqual({ + id: 1, + posts: [{ id: 1 }], + }); +}); + +test('[Find One] Get users with posts + where + partial(false)', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const usersWithPosts = await db.query.usersTable.findFirst({ + columns: { + name: false, + }, + with: { + posts: { + columns: { + content: false, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }, + }, + where: (({ id }, { eq }) => eq(id, 1)), + }); + + expectTypeOf(usersWithPosts).toEqualTypeOf< + { + id: number; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + ownerId: number | null; + createdAt: Date; + }[]; + } | undefined + >(); + + expect(usersWithPosts!.posts.length).eq(1); + + expect(usersWithPosts).toEqual({ + id: 1, + verified: false, + invitedBy: null, + posts: [{ id: 1, ownerId: 1, createdAt: usersWithPosts?.posts[0]?.createdAt }], + }); +}); + +/* + One relation users+users. Self referencing +*/ + +test.skip('Get user with invitee', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + with: { + invitee: true, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + usersWithInvitee.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(usersWithInvitee.length).eq(4); + expect(usersWithInvitee[0]?.invitee).toBeNull(); + expect(usersWithInvitee[1]?.invitee).toBeNull(); + expect(usersWithInvitee[2]?.invitee).not.toBeNull(); + expect(usersWithInvitee[3]?.invitee).not.toBeNull(); + + expect(usersWithInvitee[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[2]).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + }); + expect(usersWithInvitee[3]).toEqual({ + id: 4, + name: 'John', + verified: false, + invitedBy: 2, + invitee: { id: 2, name: 'Andrew', verified: false, invitedBy: null }, + }); +}); + +test.skip('Get user + limit with invitee', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew', invitedBy: 1 }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + with: { + invitee: true, + }, + limit: 2, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + usersWithInvitee.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(usersWithInvitee.length).eq(2); + expect(usersWithInvitee[0]?.invitee).toBeNull(); + expect(usersWithInvitee[1]?.invitee).not.toBeNull(); + + expect(usersWithInvitee[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + }); +}); + +test.skip('Get user with invitee and custom fields', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + extras: (users, { sql }) => ({ lower: sql`lower(${users.name})`.as('lower_name') }), + with: { + invitee: { + extras: (invitee, { sql }) => ({ lower: sql`lower(${invitee.name})`.as('lower_name') }), + }, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + lower: string; + invitedBy: number | null; + invitee: { + id: number; + name: string; + verified: boolean; + lower: string; + invitedBy: number | null; + } | null; + }[] + >(); + + usersWithInvitee.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(usersWithInvitee.length).eq(4); + expect(usersWithInvitee[0]?.invitee).toBeNull(); + expect(usersWithInvitee[1]?.invitee).toBeNull(); + expect(usersWithInvitee[2]?.invitee).not.toBeNull(); + expect(usersWithInvitee[3]?.invitee).not.toBeNull(); + + expect(usersWithInvitee[0]).toEqual({ + id: 1, + name: 'Dan', + lower: 'dan', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[1]).toEqual({ + id: 2, + name: 'Andrew', + lower: 'andrew', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[2]).toEqual({ + id: 3, + name: 'Alex', + lower: 'alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', lower: 'dan', verified: false, invitedBy: null }, + }); + expect(usersWithInvitee[3]).toEqual({ + id: 4, + name: 'John', + lower: 'john', + verified: false, + invitedBy: 2, + invitee: { id: 2, name: 'Andrew', lower: 'andrew', verified: false, invitedBy: null }, + }); +}); + +test.skip('Get user with invitee and custom fields + limits', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + extras: (users, { sql }) => ({ lower: sql`lower(${users.name})`.as('lower_name') }), + limit: 3, + with: { + invitee: { + extras: (invitee, { sql }) => ({ lower: sql`lower(${invitee.name})`.as('lower_name') }), + }, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + lower: string; + invitedBy: number | null; + invitee: { + id: number; + name: string; + verified: boolean; + lower: string; + invitedBy: number | null; + } | null; + }[] + >(); + + usersWithInvitee.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(usersWithInvitee.length).eq(3); + expect(usersWithInvitee[0]?.invitee).toBeNull(); + expect(usersWithInvitee[1]?.invitee).toBeNull(); + expect(usersWithInvitee[2]?.invitee).not.toBeNull(); + + expect(usersWithInvitee[0]).toEqual({ + id: 1, + name: 'Dan', + lower: 'dan', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[1]).toEqual({ + id: 2, + name: 'Andrew', + lower: 'andrew', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[2]).toEqual({ + id: 3, + name: 'Alex', + lower: 'alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', lower: 'dan', verified: false, invitedBy: null }, + }); +}); + +test.skip('Get user with invitee + order by', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + orderBy: (users, { desc }) => [desc(users.id)], + with: { + invitee: true, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + expect(usersWithInvitee.length).eq(4); + expect(usersWithInvitee[3]?.invitee).toBeNull(); + expect(usersWithInvitee[2]?.invitee).toBeNull(); + expect(usersWithInvitee[1]?.invitee).not.toBeNull(); + expect(usersWithInvitee[0]?.invitee).not.toBeNull(); + + expect(usersWithInvitee[3]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[2]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + invitee: null, + }); + expect(usersWithInvitee[1]).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + }); + expect(usersWithInvitee[0]).toEqual({ + id: 4, + name: 'John', + verified: false, + invitedBy: 2, + invitee: { id: 2, name: 'Andrew', verified: false, invitedBy: null }, + }); +}); + +test.skip('Get user with invitee + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + where: (users, { eq, or }) => (or(eq(users.id, 3), eq(users.id, 4))), + with: { + invitee: true, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + expect(usersWithInvitee.length).eq(2); + expect(usersWithInvitee[0]?.invitee).not.toBeNull(); + expect(usersWithInvitee[1]?.invitee).not.toBeNull(); + + expect(usersWithInvitee).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + }); + expect(usersWithInvitee).toContainEqual({ + id: 4, + name: 'John', + verified: false, + invitedBy: 2, + invitee: { id: 2, name: 'Andrew', verified: false, invitedBy: null }, + }); +}); + +test.skip('Get user with invitee + where + partial', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + where: (users, { eq, or }) => (or(eq(users.id, 3), eq(users.id, 4))), + columns: { + id: true, + name: true, + }, + with: { + invitee: { + columns: { + id: true, + name: true, + }, + }, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + invitee: { + id: number; + name: string; + } | null; + }[] + >(); + + expect(usersWithInvitee.length).eq(2); + expect(usersWithInvitee[0]?.invitee).not.toBeNull(); + expect(usersWithInvitee[1]?.invitee).not.toBeNull(); + + expect(usersWithInvitee).toContainEqual({ + id: 3, + name: 'Alex', + invitee: { id: 1, name: 'Dan' }, + }); + expect(usersWithInvitee).toContainEqual({ + id: 4, + name: 'John', + invitee: { id: 2, name: 'Andrew' }, + }); +}); + +test.skip('Get user with invitee + where + partial. Did not select users id, but used it in where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + where: (users, { eq, or }) => (or(eq(users.id, 3), eq(users.id, 4))), + columns: { + name: true, + }, + with: { + invitee: { + columns: { + id: true, + name: true, + }, + }, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + name: string; + invitee: { + id: number; + name: string; + } | null; + }[] + >(); + + expect(usersWithInvitee.length).eq(2); + expect(usersWithInvitee[0]?.invitee).not.toBeNull(); + expect(usersWithInvitee[1]?.invitee).not.toBeNull(); + + expect(usersWithInvitee).toContainEqual({ + name: 'Alex', + invitee: { id: 1, name: 'Dan' }, + }); + expect(usersWithInvitee).toContainEqual({ + name: 'John', + invitee: { id: 2, name: 'Andrew' }, + }); +}); + +test.skip('Get user with invitee + where + partial(true+false)', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + where: (users, { eq, or }) => (or(eq(users.id, 3), eq(users.id, 4))), + columns: { + id: true, + name: true, + verified: false, + }, + with: { + invitee: { + columns: { + id: true, + name: true, + verified: false, + }, + }, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + invitee: { + id: number; + name: string; + } | null; + }[] + >(); + + expect(usersWithInvitee.length).eq(2); + expect(usersWithInvitee[0]?.invitee).not.toBeNull(); + expect(usersWithInvitee[1]?.invitee).not.toBeNull(); + + expect(usersWithInvitee).toContainEqual({ + id: 3, + name: 'Alex', + invitee: { id: 1, name: 'Dan' }, + }); + expect(usersWithInvitee).toContainEqual({ + id: 4, + name: 'John', + invitee: { id: 2, name: 'Andrew' }, + }); +}); + +test.skip('Get user with invitee + where + partial(false)', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + const usersWithInvitee = await db.query.usersTable.findMany({ + where: (users, { eq, or }) => (or(eq(users.id, 3), eq(users.id, 4))), + columns: { + verified: false, + }, + with: { + invitee: { + columns: { + name: false, + }, + }, + }, + }); + + expectTypeOf(usersWithInvitee).toEqualTypeOf< + { + id: number; + name: string; + invitedBy: number | null; + invitee: { + id: number; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + expect(usersWithInvitee.length).eq(2); + expect(usersWithInvitee[0]?.invitee).not.toBeNull(); + expect(usersWithInvitee[1]?.invitee).not.toBeNull(); + + expect(usersWithInvitee).toContainEqual({ + id: 3, + name: 'Alex', + invitedBy: 1, + invitee: { id: 1, verified: false, invitedBy: null }, + }); + expect(usersWithInvitee).toContainEqual({ + id: 4, + name: 'John', + invitedBy: 2, + invitee: { id: 2, verified: false, invitedBy: null }, + }); +}); + +/* + Two first-level relations users+users and users+posts +*/ + +test.skip('Get user with invitee and posts', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const response = await db.query.usersTable.findMany({ + with: { + invitee: true, + posts: true, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { id: number; ownerId: number | null; content: string; createdAt: Date }[]; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).eq(4); + + expect(response[0]?.invitee).toBeNull(); + expect(response[1]?.invitee).toBeNull(); + expect(response[2]?.invitee).not.toBeNull(); + expect(response[3]?.invitee).not.toBeNull(); + + expect(response[0]?.posts.length).eq(1); + expect(response[1]?.posts.length).eq(1); + expect(response[2]?.posts.length).eq(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: response[0]?.posts[0]?.createdAt }], + }); + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 2, ownerId: 2, content: 'Post2', createdAt: response[1]?.posts[0]?.createdAt }], + }); + expect(response).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + posts: [{ id: 3, ownerId: 3, content: 'Post3', createdAt: response[2]?.posts[0]?.createdAt }], + }); + expect(response).toContainEqual({ + id: 4, + name: 'John', + verified: false, + invitedBy: 2, + invitee: { id: 2, name: 'Andrew', verified: false, invitedBy: null }, + posts: [], + }); +}); + +test.skip('Get user with invitee and posts + limit posts and users', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const response = await db.query.usersTable.findMany({ + limit: 3, + with: { + invitee: true, + posts: { + limit: 1, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { id: number; ownerId: number | null; content: string; createdAt: Date }[]; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).eq(3); + + expect(response[0]?.invitee).toBeNull(); + expect(response[1]?.invitee).toBeNull(); + expect(response[2]?.invitee).not.toBeNull(); + + expect(response[0]?.posts.length).eq(1); + expect(response[1]?.posts.length).eq(1); + expect(response[2]?.posts.length).eq(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', createdAt: response[0]?.posts[0]?.createdAt }], + }); + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 3, ownerId: 2, content: 'Post2', createdAt: response[1]?.posts[0]?.createdAt }], + }); + expect(response).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + posts: [{ id: 5, ownerId: 3, content: 'Post3', createdAt: response[2]?.posts[0]?.createdAt }], + }); +}); + +test.skip('Get user with invitee and posts + limits + custom fields in each', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const response = await db.query.usersTable.findMany({ + limit: 3, + extras: (users, { sql }) => ({ lower: sql`lower(${users.name})`.as('lower_name') }), + with: { + invitee: { + extras: (users, { sql }) => ({ lower: sql`lower(${users.name})`.as('lower_invitee_name') }), + }, + posts: { + limit: 1, + extras: (posts, { sql }) => ({ lower: sql`lower(${posts.content})`.as('lower_content') }), + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + lower: string; + invitedBy: number | null; + posts: { id: number; lower: string; ownerId: number | null; content: string; createdAt: Date }[]; + invitee: { + id: number; + name: string; + lower: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).eq(3); + + expect(response[0]?.invitee).toBeNull(); + expect(response[1]?.invitee).toBeNull(); + expect(response[2]?.invitee).not.toBeNull(); + + expect(response[0]?.posts.length).eq(1); + expect(response[1]?.posts.length).eq(1); + expect(response[2]?.posts.length).eq(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + lower: 'dan', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', lower: 'post1', createdAt: response[0]?.posts[0]?.createdAt }], + }); + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + lower: 'andrew', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 3, ownerId: 2, content: 'Post2', lower: 'post2', createdAt: response[1]?.posts[0]?.createdAt }], + }); + expect(response).toContainEqual({ + id: 3, + name: 'Alex', + lower: 'alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', lower: 'dan', verified: false, invitedBy: null }, + posts: [{ id: 5, ownerId: 3, content: 'Post3', lower: 'post3', createdAt: response[2]?.posts[0]?.createdAt }], + }); +}); + +test.skip('Get user with invitee and posts + custom fields in each', async () => { + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const response = await db.query.usersTable.findMany({ + extras: (users, { sql }) => ({ lower: sql`lower(${users.name})`.as('lower_name') }), + with: { + invitee: { + extras: (users, { sql }) => ({ lower: sql`lower(${users.name})`.as('lower_name') }), + }, + posts: { + extras: (posts, { sql }) => ({ lower: sql`lower(${posts.content})`.as('lower_name') }), + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + lower: string; + invitedBy: number | null; + posts: { id: number; lower: string; ownerId: number | null; content: string; createdAt: Date }[]; + invitee: { + id: number; + name: string; + lower: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + response[0]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + response[1]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + response[2]?.posts.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).eq(4); + + expect(response[0]?.invitee).toBeNull(); + expect(response[1]?.invitee).toBeNull(); + expect(response[2]?.invitee).not.toBeNull(); + expect(response[3]?.invitee).not.toBeNull(); + + expect(response[0]?.posts.length).eq(2); + expect(response[1]?.posts.length).eq(2); + expect(response[2]?.posts.length).eq(2); + expect(response[3]?.posts.length).eq(0); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + lower: 'dan', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 1, ownerId: 1, content: 'Post1', lower: 'post1', createdAt: response[0]?.posts[0]?.createdAt }, { + id: 2, + ownerId: 1, + content: 'Post1.1', + lower: 'post1.1', + createdAt: response[0]?.posts[1]?.createdAt, + }], + }); + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + lower: 'andrew', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 3, ownerId: 2, content: 'Post2', lower: 'post2', createdAt: response[1]?.posts[0]?.createdAt }, { + id: 4, + ownerId: 2, + content: 'Post2.1', + lower: 'post2.1', + createdAt: response[1]?.posts[1]?.createdAt, + }], + }); + expect(response).toContainEqual({ + id: 3, + name: 'Alex', + lower: 'alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', lower: 'dan', verified: false, invitedBy: null }, + posts: [{ id: 5, ownerId: 3, content: 'Post3', lower: 'post3', createdAt: response[2]?.posts[0]?.createdAt }, { + id: 6, + ownerId: 3, + content: 'Post3.1', + lower: 'post3.1', + createdAt: response[2]?.posts[1]?.createdAt, + }], + }); + expect(response).toContainEqual({ + id: 4, + name: 'John', + lower: 'john', + verified: false, + invitedBy: 2, + invitee: { id: 2, name: 'Andrew', lower: 'andrew', verified: false, invitedBy: null }, + posts: [], + }); +}); + +test.skip('Get user with invitee and posts + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const response = await db.query.usersTable.findMany({ + orderBy: (users, { desc }) => [desc(users.id)], + with: { + invitee: true, + posts: { + orderBy: (posts, { desc }) => [desc(posts.id)], + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { id: number; ownerId: number | null; content: string; createdAt: Date }[]; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + expect(response.length).eq(4); + + expect(response[3]?.invitee).toBeNull(); + expect(response[2]?.invitee).toBeNull(); + expect(response[1]?.invitee).not.toBeNull(); + expect(response[0]?.invitee).not.toBeNull(); + + expect(response[0]?.posts.length).eq(0); + expect(response[1]?.posts.length).eq(1); + expect(response[2]?.posts.length).eq(2); + expect(response[3]?.posts.length).eq(2); + + expect(response[3]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 2, ownerId: 1, content: 'Post1.1', createdAt: response[3]?.posts[0]?.createdAt }, { + id: 1, + ownerId: 1, + content: 'Post1', + createdAt: response[3]?.posts[1]?.createdAt, + }], + }); + expect(response[2]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 4, ownerId: 2, content: 'Post2.1', createdAt: response[2]?.posts[0]?.createdAt }, { + id: 3, + ownerId: 2, + content: 'Post2', + createdAt: response[2]?.posts[1]?.createdAt, + }], + }); + expect(response[1]).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + posts: [{ + id: 5, + ownerId: 3, + content: 'Post3', + createdAt: response[3]?.posts[1]?.createdAt, + }], + }); + expect(response[0]).toEqual({ + id: 4, + name: 'John', + verified: false, + invitedBy: 2, + invitee: { id: 2, name: 'Andrew', verified: false, invitedBy: null }, + posts: [], + }); +}); + +test.skip('Get user with invitee and posts + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const response = await db.query.usersTable.findMany({ + where: (users, { eq, or }) => (or(eq(users.id, 2), eq(users.id, 3))), + with: { + invitee: true, + posts: { + where: (posts, { eq }) => (eq(posts.ownerId, 2)), + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { id: number; ownerId: number | null; content: string; createdAt: Date }[]; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).eq(2); + + expect(response[0]?.invitee).toBeNull(); + expect(response[1]?.invitee).not.toBeNull(); + + expect(response[0]?.posts.length).eq(1); + expect(response[1]?.posts.length).eq(0); + + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + invitee: null, + posts: [{ id: 2, ownerId: 2, content: 'Post2', createdAt: response[0]?.posts[0]?.createdAt }], + }); + expect(response).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + posts: [], + }); +}); + +test.skip('Get user with invitee and posts + limit posts and users + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + { ownerId: 3, content: 'Post3.1' }, + ]); + + const response = await db.query.usersTable.findMany({ + where: (users, { eq, or }) => (or(eq(users.id, 3), eq(users.id, 4))), + limit: 1, + with: { + invitee: true, + posts: { + where: (posts, { eq }) => (eq(posts.ownerId, 3)), + limit: 1, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { id: number; ownerId: number | null; content: string; createdAt: Date }[]; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + expect(response.length).eq(1); + + expect(response[0]?.invitee).not.toBeNull(); + expect(response[0]?.posts.length).eq(1); + + expect(response).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + posts: [{ id: 5, ownerId: 3, content: 'Post3', createdAt: response[0]?.posts[0]?.createdAt }], + }); +}); + +test.skip('Get user with invitee and posts + orderBy + where + custom', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const response = await db.query.usersTable.findMany({ + orderBy: [desc(usersTable.id)], + where: or(eq(usersTable.id, 3), eq(usersTable.id, 4)), + extras: { + lower: sql`lower(${usersTable.name})`.as('lower_name'), + }, + with: { + invitee: true, + posts: { + where: eq(postsTable.ownerId, 3), + orderBy: [desc(postsTable.id)], + extras: { + lower: sql`lower(${postsTable.content})`.as('lower_name'), + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + lower: string; + posts: { id: number; lower: string; ownerId: number | null; content: string; createdAt: Date }[]; + invitee: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[] + >(); + + expect(response.length).eq(2); + + expect(response[1]?.invitee).not.toBeNull(); + expect(response[0]?.invitee).not.toBeNull(); + + expect(response[0]?.posts.length).eq(0); + expect(response[1]?.posts.length).eq(1); + + expect(response[1]).toEqual({ + id: 3, + name: 'Alex', + lower: 'alex', + verified: false, + invitedBy: 1, + invitee: { id: 1, name: 'Dan', verified: false, invitedBy: null }, + posts: [{ + id: 5, + ownerId: 3, + content: 'Post3', + lower: 'post3', + createdAt: response[1]?.posts[0]?.createdAt, + }], + }); + expect(response[0]).toEqual({ + id: 4, + name: 'John', + lower: 'john', + verified: false, + invitedBy: 2, + invitee: { id: 2, name: 'Andrew', verified: false, invitedBy: null }, + posts: [], + }); +}); + +test.skip('Get user with invitee and posts + orderBy + where + partial + custom', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex', invitedBy: 1 }, + { id: 4, name: 'John', invitedBy: 2 }, + ]); + + await db.insert(postsTable).values([ + { ownerId: 1, content: 'Post1' }, + { ownerId: 1, content: 'Post1.1' }, + { ownerId: 2, content: 'Post2' }, + { ownerId: 2, content: 'Post2.1' }, + { ownerId: 3, content: 'Post3' }, + ]); + + const response = await db.query.usersTable.findMany({ + orderBy: [desc(usersTable.id)], + where: or(eq(usersTable.id, 3), eq(usersTable.id, 4)), + extras: { + lower: sql`lower(${usersTable.name})`.as('lower_name'), + }, + columns: { + id: true, + name: true, + }, + with: { + invitee: { + columns: { + id: true, + name: true, + }, + extras: { + lower: sql`lower(${usersTable.name})`.as('lower_name'), + }, + }, + posts: { + columns: { + id: true, + content: true, + }, + where: eq(postsTable.ownerId, 3), + orderBy: [desc(postsTable.id)], + extras: { + lower: sql`lower(${postsTable.content})`.as('lower_name'), + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + lower: string; + posts: { id: number; lower: string; content: string }[]; + invitee: { + id: number; + name: string; + lower: string; + } | null; + }[] + >(); + + expect(response.length).eq(2); + + expect(response[1]?.invitee).not.toBeNull(); + expect(response[0]?.invitee).not.toBeNull(); + + expect(response[0]?.posts.length).eq(0); + expect(response[1]?.posts.length).eq(1); + + expect(response[1]).toEqual({ + id: 3, + name: 'Alex', + lower: 'alex', + invitee: { id: 1, name: 'Dan', lower: 'dan' }, + posts: [{ + id: 5, + content: 'Post3', + lower: 'post3', + }], + }); + expect(response[0]).toEqual({ + id: 4, + name: 'John', + lower: 'john', + invitee: { id: 2, name: 'Andrew', lower: 'andrew' }, + posts: [], + }); +}); + +/* + One two-level relation users+posts+comments +*/ + +test.skip('Get user with posts and posts with comments', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { id: 1, ownerId: 1, content: 'Post1' }, + { id: 2, ownerId: 2, content: 'Post2' }, + { id: 3, ownerId: 3, content: 'Post3' }, + ]); + + await db.insert(commentsTable).values([ + { postId: 1, content: 'Comment1', creator: 2 }, + { postId: 2, content: 'Comment2', creator: 2 }, + { postId: 3, content: 'Comment3', creator: 3 }, + ]); + + const response = await db.query.usersTable.findMany({ + with: { + posts: { + with: { + comments: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + comments: { + id: number; + content: string; + createdAt: Date; + creator: number | null; + postId: number | null; + }[]; + }[]; + }[] + >(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).eq(3); + expect(response[0]?.posts.length).eq(1); + expect(response[1]?.posts.length).eq(1); + expect(response[2]?.posts.length).eq(1); + + expect(response[0]?.posts[0]?.comments.length).eq(1); + expect(response[1]?.posts[0]?.comments.length).eq(1); + expect(response[2]?.posts[0]?.comments.length).eq(1); + + expect(response[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ + id: 1, + ownerId: 1, + content: 'Post1', + createdAt: response[0]?.posts[0]?.createdAt, + comments: [ + { + id: 1, + content: 'Comment1', + creator: 2, + postId: 1, + createdAt: response[0]?.posts[0]?.comments[0]?.createdAt, + }, + ], + }], + }); + expect(response[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + posts: [{ + id: 2, + ownerId: 2, + content: 'Post2', + createdAt: response[1]?.posts[0]?.createdAt, + comments: [ + { + id: 2, + content: 'Comment2', + creator: 2, + postId: 2, + createdAt: response[1]?.posts[0]?.comments[0]?.createdAt, + }, + ], + }], + }); + // expect(response[2]).toEqual({ + // id: 3, + // name: 'Alex', + // verified: false, + // invitedBy: null, + // posts: [{ + // id: 3, + // ownerId: 3, + // content: 'Post3', + // createdAt: response[2]?.posts[0]?.createdAt, + // comments: [ + // { + // id: , + // content: 'Comment3', + // creator: 3, + // postId: 3, + // createdAt: response[2]?.posts[0]?.comments[0]?.createdAt, + // }, + // ], + // }], + // }); +}); + +// Get user with limit posts and limit comments + +// Get user with custom field + post + comment with custom field + +// Get user with limit + posts orderBy + comment orderBy + +// Get user with where + posts where + comment where + +// Get user with where + posts partial where + comment where + +// Get user with where + posts partial where + comment partial(false) where + +// Get user with where partial(false) + posts partial where partial(false) + comment partial(false+true) where + +// Get user with where + posts partial where + comment where. Didn't select field from where in posts + +// Get user with where + posts partial where + comment where. Didn't select field from where for all + +// Get with limit+offset in each + +/* + One two-level + One first-level relation users+posts+comments and users+users +*/ + +/* + One three-level relation users+posts+comments+comment_owner +*/ + +test.skip('Get user with posts and posts with comments and comments with owner', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { id: 1, ownerId: 1, content: 'Post1' }, + { id: 2, ownerId: 2, content: 'Post2' }, + { id: 3, ownerId: 3, content: 'Post3' }, + ]); + + await db.insert(commentsTable).values([ + { postId: 1, content: 'Comment1', creator: 2 }, + { postId: 2, content: 'Comment2', creator: 2 }, + { postId: 3, content: 'Comment3', creator: 3 }, + ]); + + const response = await db.query.usersTable.findMany({ + with: { + posts: { + with: { + comments: { + with: { + author: true, + }, + }, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + comments: { + id: number; + content: string; + createdAt: Date; + creator: number | null; + postId: number | null; + author: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[]; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).eq(3); + expect(response[0]?.posts.length).eq(1); + expect(response[1]?.posts.length).eq(1); + expect(response[2]?.posts.length).eq(1); + + expect(response[0]?.posts[0]?.comments.length).eq(1); + expect(response[1]?.posts[0]?.comments.length).eq(1); + expect(response[2]?.posts[0]?.comments.length).eq(1); + + expect(response[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ + id: 1, + ownerId: 1, + content: 'Post1', + createdAt: response[0]?.posts[0]?.createdAt, + comments: [ + { + id: 1, + content: 'Comment1', + creator: 2, + author: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + postId: 1, + createdAt: response[0]?.posts[0]?.comments[0]?.createdAt, + }, + ], + }], + }); + expect(response[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + posts: [{ + id: 2, + ownerId: 2, + content: 'Post2', + createdAt: response[1]?.posts[0]?.createdAt, + comments: [ + { + id: 2, + content: 'Comment2', + creator: 2, + author: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + postId: 2, + createdAt: response[1]?.posts[0]?.comments[0]?.createdAt, + }, + ], + }], + }); +}); + +test.skip('Get user with posts and posts with comments and comments with owner where exists', async () => { + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(postsTable).values([ + { id: 1, ownerId: 1, content: 'Post1' }, + { id: 2, ownerId: 2, content: 'Post2' }, + { id: 3, ownerId: 3, content: 'Post3' }, + ]); + + await db.insert(commentsTable).values([ + { postId: 1, content: 'Comment1', creator: 2 }, + { postId: 2, content: 'Comment2', creator: 2 }, + { postId: 3, content: 'Comment3', creator: 3 }, + ]); + + const response = await db.query.usersTable.findMany({ + with: { + posts: { + with: { + comments: { + with: { + author: true, + }, + }, + }, + }, + }, + where: (table, { exists, eq }) => exists(db.select({ one: sql`1` }).from(usersTable).where(eq(sql`1`, table.id))), + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + posts: { + id: number; + content: string; + ownerId: number | null; + createdAt: Date; + comments: { + id: number; + content: string; + createdAt: Date; + creator: number | null; + postId: number | null; + author: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + } | null; + }[]; + }[]; + }[]>(); + + expect(response.length).eq(1); + expect(response[0]?.posts.length).eq(1); + + expect(response[0]?.posts[0]?.comments.length).eq(1); + + expect(response[0]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + posts: [{ + id: 1, + ownerId: 1, + content: 'Post1', + createdAt: response[0]?.posts[0]?.createdAt, + comments: [ + { + id: 1, + content: 'Comment1', + creator: 2, + author: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + postId: 1, + createdAt: response[0]?.posts[0]?.comments[0]?.createdAt, + }, + ], + }], + }); +}); + +/* + One three-level relation + 1 first-level relatioon + 1. users+posts+comments+comment_owner + 2. users+users +*/ + +/* + One four-level relation users+posts+comments+coment_likes +*/ + +/* + [Find Many] Many-to-many cases + + Users+users_to_groups+groups +*/ + +test.skip('[Find Many] Get users with groups', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findMany({ + with: { + usersToGroups: { + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(3); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(1); + expect(response[2]?.usersToGroups.length).toEqual(2); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 1, + name: 'Group1', + description: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 2, + name: 'Group2', + description: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 3, + name: 'Group3', + description: null, + }, + }, { + group: { + id: 2, + name: 'Group2', + description: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get groups with users', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findMany({ + with: { + usersToGroups: { + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(3); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(2); + expect(response[2]?.usersToGroups.length).toEqual(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Group1', + description: null, + usersToGroups: [{ + user: { + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 2, + name: 'Group2', + description: null, + usersToGroups: [{ + user: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + }, { + user: { + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 3, + name: 'Group3', + description: null, + usersToGroups: [{ + user: { + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get users with groups + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 2, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findMany({ + limit: 2, + with: { + usersToGroups: { + limit: 1, + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(2); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 1, + name: 'Group1', + description: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 2, + name: 'Group2', + description: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get groups with users + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findMany({ + limit: 2, + with: { + usersToGroups: { + limit: 1, + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(2); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Group1', + description: null, + usersToGroups: [{ + user: { + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 2, + name: 'Group2', + description: null, + usersToGroups: [{ + user: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get users with groups + limit + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 2, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findMany({ + limit: 1, + where: (_, { eq, or }) => or(eq(usersTable.id, 1), eq(usersTable.id, 2)), + with: { + usersToGroups: { + where: eq(usersToGroupsTable.groupId, 1), + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(1); + + expect(response[0]?.usersToGroups.length).toEqual(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 1, + name: 'Group1', + description: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get groups with users + limit + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findMany({ + limit: 1, + where: gt(groupsTable.id, 1), + with: { + usersToGroups: { + where: eq(usersToGroupsTable.userId, 2), + limit: 1, + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(1); + + expect(response[0]?.usersToGroups.length).toEqual(1); + + expect(response).toContainEqual({ + id: 2, + name: 'Group2', + description: null, + usersToGroups: [{ + user: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get users with groups + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 2, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findMany({ + where: (_, { eq, or }) => or(eq(usersTable.id, 1), eq(usersTable.id, 2)), + with: { + usersToGroups: { + where: eq(usersToGroupsTable.groupId, 2), + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(2); + + expect(response[0]?.usersToGroups.length).toEqual(0); + expect(response[1]?.usersToGroups.length).toEqual(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [], + }); + + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 2, + name: 'Group2', + description: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get groups with users + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findMany({ + where: gt(groupsTable.id, 1), + with: { + usersToGroups: { + where: eq(usersToGroupsTable.userId, 2), + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + }[]>(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(2); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(0); + + expect(response).toContainEqual({ + id: 2, + name: 'Group2', + description: null, + usersToGroups: [{ + user: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 3, + name: 'Group3', + description: null, + usersToGroups: [], + }); +}); + +test.skip('[Find Many] Get users with groups + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findMany({ + orderBy: (users, { desc }) => [desc(users.id)], + with: { + usersToGroups: { + orderBy: [desc(usersToGroupsTable.groupId)], + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + }[]>(); + + expect(response.length).toEqual(3); + + expect(response[0]?.usersToGroups.length).toEqual(2); + expect(response[1]?.usersToGroups.length).toEqual(1); + expect(response[2]?.usersToGroups.length).toEqual(1); + + expect(response[2]).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 1, + name: 'Group1', + description: null, + }, + }], + }); + + expect(response[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 2, + name: 'Group2', + description: null, + }, + }], + }); + + expect(response[0]).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 3, + name: 'Group3', + description: null, + }, + }, { + group: { + id: 2, + name: 'Group2', + description: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get groups with users + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findMany({ + orderBy: [desc(groupsTable.id)], + with: { + usersToGroups: { + orderBy: (utg, { desc }) => [desc(utg.userId)], + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + }[]>(); + + expect(response.length).toEqual(3); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(2); + expect(response[2]?.usersToGroups.length).toEqual(1); + + expect(response[2]).toEqual({ + id: 1, + name: 'Group1', + description: null, + usersToGroups: [{ + user: { + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response[1]).toEqual({ + id: 2, + name: 'Group2', + description: null, + usersToGroups: [{ + user: { + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + }, + }, { + user: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response[0]).toEqual({ + id: 3, + name: 'Group3', + description: null, + usersToGroups: [{ + user: { + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find Many] Get users with groups + orderBy + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findMany({ + orderBy: (users, { desc }) => [desc(users.id)], + limit: 2, + with: { + usersToGroups: { + limit: 1, + orderBy: [desc(usersToGroupsTable.groupId)], + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf<{ + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + }[]>(); + + expect(response.length).toEqual(2); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(1); + + expect(response[1]).toEqual({ + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 2, + name: 'Group2', + description: null, + }, + }], + }); + + expect(response[0]).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 3, + name: 'Group3', + description: null, + }, + }], + }); +}); + +/* + [Find One] Many-to-many cases + + Users+users_to_groups+groups +*/ + +test.skip('[Find One] Get users with groups', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findFirst({ + with: { + usersToGroups: { + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 1, + name: 'Group1', + description: null, + }, + }], + }); +}); + +test.skip('[Find One] Get groups with users', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findFirst({ + with: { + usersToGroups: { + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 1, + name: 'Group1', + description: null, + usersToGroups: [{ + user: { + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find One] Get users with groups + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 2, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findFirst({ + with: { + usersToGroups: { + limit: 1, + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 1, + name: 'Group1', + description: null, + }, + }], + }); +}); + +test.skip('[Find One] Get groups with users + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findFirst({ + with: { + usersToGroups: { + limit: 1, + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 1, + name: 'Group1', + description: null, + usersToGroups: [{ + user: { + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find One] Get users with groups + limit + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 2, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findFirst({ + where: (_, { eq, or }) => or(eq(usersTable.id, 1), eq(usersTable.id, 2)), + with: { + usersToGroups: { + where: eq(usersToGroupsTable.groupId, 1), + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 1, + name: 'Group1', + description: null, + }, + }], + }); +}); + +test.skip('[Find One] Get groups with users + limit + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findFirst({ + where: gt(groupsTable.id, 1), + with: { + usersToGroups: { + where: eq(usersToGroupsTable.userId, 2), + limit: 1, + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 2, + name: 'Group2', + description: null, + usersToGroups: [{ + user: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find One] Get users with groups + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 2, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findFirst({ + where: (_, { eq, or }) => or(eq(usersTable.id, 1), eq(usersTable.id, 2)), + with: { + usersToGroups: { + where: eq(usersToGroupsTable.groupId, 2), + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(0); + + expect(response).toEqual({ + id: 1, + name: 'Dan', + verified: false, + invitedBy: null, + usersToGroups: [], + }); +}); + +test.skip('[Find One] Get groups with users + where', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findFirst({ + where: gt(groupsTable.id, 1), + with: { + usersToGroups: { + where: eq(usersToGroupsTable.userId, 2), + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 2, + name: 'Group2', + description: null, + usersToGroups: [{ + user: { + id: 2, + name: 'Andrew', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find One] Get users with groups + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findFirst({ + orderBy: (users, { desc }) => [desc(users.id)], + with: { + usersToGroups: { + orderBy: [desc(usersToGroupsTable.groupId)], + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(2); + + expect(response).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 3, + name: 'Group3', + description: null, + }, + }, { + group: { + id: 2, + name: 'Group2', + description: null, + }, + }], + }); +}); + +test.skip('[Find One] Get groups with users + orderBy', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findFirst({ + orderBy: [desc(groupsTable.id)], + with: { + usersToGroups: { + orderBy: (utg, { desc }) => [desc(utg.userId)], + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 3, + name: 'Group3', + description: null, + usersToGroups: [{ + user: { + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('[Find One] Get users with groups + orderBy + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findFirst({ + orderBy: (users, { desc }) => [desc(users.id)], + with: { + usersToGroups: { + limit: 1, + orderBy: [desc(usersToGroupsTable.groupId)], + columns: {}, + with: { + group: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + }; + }[]; + } | undefined + >(); + + expect(response?.usersToGroups.length).toEqual(1); + + expect(response).toEqual({ + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 3, + name: 'Group3', + description: null, + }, + }], + }); +}); + +test.skip('Get groups with users + orderBy + limit', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findMany({ + orderBy: [desc(groupsTable.id)], + limit: 2, + with: { + usersToGroups: { + limit: 1, + orderBy: (utg, { desc }) => [desc(utg.userId)], + columns: {}, + with: { + user: true, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + description: string | null; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + }; + }[]; + }[] + >(); + + expect(response.length).toEqual(2); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(1); + + expect(response[1]).toEqual({ + id: 2, + name: 'Group2', + description: null, + usersToGroups: [{ + user: { + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response[0]).toEqual({ + id: 3, + name: 'Group3', + description: null, + usersToGroups: [{ + user: { + id: 3, + name: 'Alex', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test.skip('Get users with groups + custom', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.usersTable.findMany({ + extras: { + lower: sql`lower(${usersTable.name})`.as('lower_name'), + }, + with: { + usersToGroups: { + columns: {}, + with: { + group: { + extras: { + lower: sql`lower(${groupsTable.name})`.as('lower_name'), + }, + }, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + lower: string; + usersToGroups: { + group: { + id: number; + name: string; + description: string | null; + lower: string; + }; + }[]; + }[] + >(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(3); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(1); + expect(response[2]?.usersToGroups.length).toEqual(2); + + expect(response).toContainEqual({ + id: 1, + name: 'Dan', + lower: 'dan', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 1, + name: 'Group1', + lower: 'group1', + description: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 2, + name: 'Andrew', + lower: 'andrew', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 2, + name: 'Group2', + lower: 'group2', + description: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 3, + name: 'Alex', + lower: 'alex', + verified: false, + invitedBy: null, + usersToGroups: [{ + group: { + id: 3, + name: 'Group3', + lower: 'group3', + description: null, + }, + }, { + group: { + id: 2, + name: 'Group2', + lower: 'group2', + description: null, + }, + }], + }); +}); + +test.skip('Get groups with users + custom', async (t) => { + const { singlestoreDb: db } = t; + + await db.insert(usersTable).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(groupsTable).values([ + { id: 1, name: 'Group1' }, + { id: 2, name: 'Group2' }, + { id: 3, name: 'Group3' }, + ]); + + await db.insert(usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 2, groupId: 2 }, + { userId: 3, groupId: 3 }, + { userId: 3, groupId: 2 }, + ]); + + const response = await db.query.groupsTable.findMany({ + extras: (table, { sql }) => ({ + lower: sql`lower(${table.name})`.as('lower_name'), + }), + with: { + usersToGroups: { + columns: {}, + with: { + user: { + extras: (table, { sql }) => ({ + lower: sql`lower(${table.name})`.as('lower_name'), + }), + }, + }, + }, + }, + }); + + expectTypeOf(response).toEqualTypeOf< + { + id: number; + name: string; + description: string | null; + lower: string; + usersToGroups: { + user: { + id: number; + name: string; + verified: boolean; + invitedBy: number | null; + lower: string; + }; + }[]; + }[] + >(); + + response.sort((a, b) => (a.id > b.id) ? 1 : -1); + + expect(response.length).toEqual(3); + + expect(response[0]?.usersToGroups.length).toEqual(1); + expect(response[1]?.usersToGroups.length).toEqual(2); + expect(response[2]?.usersToGroups.length).toEqual(1); + + expect(response).toContainEqual({ + id: 1, + name: 'Group1', + lower: 'group1', + description: null, + usersToGroups: [{ + user: { + id: 1, + name: 'Dan', + lower: 'dan', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 2, + name: 'Group2', + lower: 'group2', + description: null, + usersToGroups: [{ + user: { + id: 2, + name: 'Andrew', + lower: 'andrew', + verified: false, + invitedBy: null, + }, + }, { + user: { + id: 3, + name: 'Alex', + lower: 'alex', + verified: false, + invitedBy: null, + }, + }], + }); + + expect(response).toContainEqual({ + id: 3, + name: 'Group3', + lower: 'group3', + description: null, + usersToGroups: [{ + user: { + id: 3, + name: 'Alex', + lower: 'alex', + verified: false, + invitedBy: null, + }, + }], + }); +}); + +test('.toSQL()', () => { + const query = db.query.usersTable.findFirst().toSQL(); + + expect(query).toHaveProperty('sql', expect.any(String)); + expect(query).toHaveProperty('params', expect.any(Array)); +}); + +// + custom + where + orderby + +// + custom + where + orderby + limit + +// + partial + +// + partial(false) + +// + partial + orderBy + where (all not selected) + +/* + One four-level relation users+posts+comments+coment_likes + + users+users_to_groups+groups +*/ + +/* + Really hard case + 1. users+posts+comments+coment_likes + 2. users+users_to_groups+groups + 3. users+users +*/ diff --git a/integration-tests/tests/replicas/singlestore.test.ts b/integration-tests/tests/replicas/singlestore.test.ts new file mode 100644 index 000000000..76d84c972 --- /dev/null +++ b/integration-tests/tests/replicas/singlestore.test.ts @@ -0,0 +1,805 @@ +import { sql } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/singlestore'; +import { boolean, serial, singlestoreTable, text, withReplicas } from 'drizzle-orm/singlestore-core'; +import { describe, expect, it, vi } from 'vitest'; + +const usersTable = singlestoreTable('users', { + id: serial('id' as string).primaryKey(), + name: text('name').notNull(), + verified: boolean('verified').notNull().default(false), +}); + +const users = singlestoreTable('users', { + id: serial('id' as string).primaryKey(), +}); + +describe('[select] read replicas singlestore', () => { + it('primary select', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb, 'select'); + const spyRead1 = vi.spyOn(read1, 'select'); + const spyRead2 = vi.spyOn(read2, 'select'); + + const query = db.$primary.select().from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(query.toSQL().sql).toEqual('select `id` from `users`'); + + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + }); + + it('random replica select', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); + + const db = withReplicas(primaryDb, [read1, read2], () => { + return randomMockReplica(); + }); + + const spyPrimary = vi.spyOn(primaryDb, 'select'); + const spyRead1 = vi.spyOn(read1, 'select'); + const spyRead2 = vi.spyOn(read2, 'select'); + + const query1 = db.select({ count: sql`count(*)`.as('count') }).from(users).limit(1); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + + expect(query1.toSQL().sql).toEqual('select count(*) as `count` from `users` limit ?'); + + const query2 = db.select().from(users); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select `id` from `users`'); + }); + + it('single read replica select', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb, 'select'); + const spyRead1 = vi.spyOn(read1, 'select'); + + const query1 = db.select().from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select `id` from `users`'); + + const query2 = db.select().from(users); + expect(spyRead1).toHaveBeenCalledTimes(2); + expect(query2.toSQL().sql).toEqual('select `id` from `users`'); + }); + + it('single read replica select + primary select', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb, 'select'); + const spyRead1 = vi.spyOn(read1, 'select'); + + const query1 = db.select({ id: users.id }).from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select `id` from `users`'); + + const query2 = db.$primary.select().from(users); + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select `id` from `users`'); + }); + + it('always first read select', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2], (replicas) => { + return replicas[0]!; + }); + + const spyPrimary = vi.spyOn(primaryDb, 'select'); + const spyRead1 = vi.spyOn(read1, 'select'); + const spyRead2 = vi.spyOn(read2, 'select'); + + const query1 = db.select().from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select `id` from `users`'); + + const query2 = db.select().from(users); + + expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('select `id` from `users`'); + }); +}); + +describe('[selectDistinct] read replicas singlestore', () => { + it('primary selectDistinct', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); + const spyRead1 = vi.spyOn(read1, 'selectDistinct'); + const spyRead2 = vi.spyOn(read2, 'selectDistinct'); + + const query = db.$primary.selectDistinct().from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query.toSQL().sql).toEqual('select distinct `id` from `users`'); + }); + + it('random replica selectDistinct', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); + + const db = withReplicas(primaryDb, [read1, read2], () => { + return randomMockReplica(); + }); + + const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); + const spyRead1 = vi.spyOn(read1, 'selectDistinct'); + const spyRead2 = vi.spyOn(read2, 'selectDistinct'); + + const query1 = db.selectDistinct().from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select distinct `id` from `users`'); + + const query2 = db.selectDistinct().from(users); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select distinct `id` from `users`'); + }); + + it('single read replica selectDistinct', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); + const spyRead1 = vi.spyOn(read1, 'selectDistinct'); + + const query1 = db.selectDistinct().from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select distinct `id` from `users`'); + + const query2 = db.selectDistinct().from(users); + expect(spyRead1).toHaveBeenCalledTimes(2); + expect(query2.toSQL().sql).toEqual('select distinct `id` from `users`'); + }); + + it('single read replica selectDistinct + primary selectDistinct', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); + const spyRead1 = vi.spyOn(read1, 'selectDistinct'); + + const query1 = db.selectDistinct().from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select distinct `id` from `users`'); + + const query2 = db.$primary.selectDistinct().from(users); + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select distinct `id` from `users`'); + }); + + it('always first read selectDistinct', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2], (replicas) => { + return replicas[0]!; + }); + + const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); + const spyRead1 = vi.spyOn(read1, 'selectDistinct'); + const spyRead2 = vi.spyOn(read2, 'selectDistinct'); + + const query1 = db.selectDistinct().from(users); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select distinct `id` from `users`'); + + const query2 = db.selectDistinct().from(users); + expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('select distinct `id` from `users`'); + }); +}); + +describe('[with] read replicas singlestore', () => { + it('primary with', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb, 'with'); + const spyRead1 = vi.spyOn(read1, 'with'); + const spyRead2 = vi.spyOn(read2, 'with'); + const obj1 = {} as any; + const obj2 = {} as any; + const obj3 = {} as any; + const obj4 = {} as any; + + db.$primary.with(obj1, obj2, obj3, obj4); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj1, obj2, obj3, obj4); + }); + + it('random replica with', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); + + const db = withReplicas(primaryDb, [read1, read2], () => { + return randomMockReplica(); + }); + + const spyPrimary = vi.spyOn(primaryDb, 'with'); + const spyRead1 = vi.spyOn(read1, 'with'); + const spyRead2 = vi.spyOn(read2, 'with'); + + db.with(); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + + db.with(); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(1); + }); + + it('single read replica with', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb, 'with'); + const spyRead1 = vi.spyOn(read1, 'with'); + + db.with(); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + + db.with(); + expect(spyRead1).toHaveBeenCalledTimes(2); + }); + + it('single read replica with + primary with', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb, 'with'); + const spyRead1 = vi.spyOn(read1, 'with'); + + db.with(); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + + db.$primary.with(); + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(1); + }); + + it('always first read with', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2], (replicas) => { + return replicas[0]!; + }); + + const spyPrimary = vi.spyOn(primaryDb, 'with'); + const spyRead1 = vi.spyOn(read1, 'with'); + const spyRead2 = vi.spyOn(read2, 'with'); + const obj1 = {} as any; + const obj2 = {} as any; + const obj3 = {} as any; + + db.with(obj1); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj1); + + db.with(obj2, obj3); + expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj2, obj3); + }); +}); + +describe('[update] replicas singlestore', () => { + it('primary update', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb, 'update'); + const spyRead1 = vi.spyOn(read1, 'update'); + const spyRead2 = vi.spyOn(read2, 'update'); + + const query1 = db.update(users).set({ id: 1 }); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('update `users` set `id` = ?'); + + const query2 = db.update(users).set({ id: 1 }); + + expect(spyPrimary).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('update `users` set `id` = ?'); + + const query3 = db.$primary.update(users).set({ id: 1 }); + + expect(spyPrimary).toHaveBeenCalledTimes(3); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query3.toSQL().sql).toEqual('update `users` set `id` = ?'); + }); +}); + +describe('[delete] replicas singlestore', () => { + it('primary delete', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb, 'delete'); + const spyRead1 = vi.spyOn(read1, 'delete'); + const spyRead2 = vi.spyOn(read2, 'delete'); + + const query1 = db.delete(users); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(users); + expect(query1.toSQL().sql).toEqual('delete from `users`'); + + const query2 = db.delete(users); + + expect(spyPrimary).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, users); + expect(query2.toSQL().sql).toEqual('delete from `users`'); + + db.$primary.delete({} as any); + + expect(spyPrimary).toHaveBeenCalledTimes(3); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + }); +}); + +describe('[insert] replicas singlestore', () => { + it('primary insert', () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb, 'insert'); + const spyRead1 = vi.spyOn(read1, 'insert'); + const spyRead2 = vi.spyOn(read2, 'insert'); + + const query = db.insert(users).values({ id: 1 }); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(users); + expect(query.toSQL().sql).toEqual('insert into `users` (`id`) values (?)'); + + db.insert(users); + + expect(spyPrimary).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, users); + + db.$primary.insert({} as any); + + expect(spyPrimary).toHaveBeenCalledTimes(3); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + }); +}); + +describe('[execute] replicas singlestore', () => { + it('primary execute', async () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb, 'execute'); + const spyRead1 = vi.spyOn(read1, 'execute'); + const spyRead2 = vi.spyOn(read2, 'execute'); + + expect(db.execute(sql``)).rejects.toThrow(); + + // try { + // db.execute(sql``); + // } catch { /* empty */ } + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + + expect(db.execute(sql``)).rejects.toThrow(); + // try { + // db.execute(sql``); + // } catch { /* empty */ } + + expect(spyPrimary).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + + expect(db.execute(sql``)).rejects.toThrow(); + // try { + // db.execute(sql``); + // } catch { /* empty */ } + + expect(spyPrimary).toHaveBeenCalledTimes(3); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + }); +}); + +describe('[transaction] replicas singlestore', () => { + it('primary transaction', async () => { + const primaryDb = drizzle({} as any); + const read1 = drizzle({} as any); + const read2 = drizzle({} as any); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb, 'transaction'); + const spyRead1 = vi.spyOn(read1, 'transaction'); + const spyRead2 = vi.spyOn(read2, 'transaction'); + const txFn1 = async (tx: any) => { + tx.select().from({} as any); + }; + + expect(db.transaction(txFn1)).rejects.toThrow(); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(txFn1); + + const txFn2 = async (tx: any) => { + tx.select().from({} as any); + }; + + expect(db.transaction(txFn2)).rejects.toThrow(); + + expect(spyPrimary).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, txFn2); + + expect(db.transaction(async (tx) => { + tx.select().from({} as any); + })).rejects.toThrow(); + + expect(spyPrimary).toHaveBeenCalledTimes(3); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + }); +}); + +describe('[findFirst] read replicas singlestore', () => { + it('primary findFirst', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + const read2 = drizzle({} as any, { schema: { usersTable } }); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); + const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + const obj = {} as any; + + db.$primary.query.usersTable.findFirst(obj); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj); + }); + + it('random replica findFirst', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + const read2 = drizzle({} as any, { schema: { usersTable } }); + + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); + + const db = withReplicas(primaryDb, [read1, read2], () => { + return randomMockReplica(); + }); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); + const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + const par1 = {} as any; + + db.query.usersTable.findFirst(par1); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(par1); + + const query = db.query.usersTable.findFirst(); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable` limit ?'); + }); + + it('single read replica findFirst', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); + + db.query.usersTable.findFirst(); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + + db.query.usersTable.findFirst(); + expect(spyRead1).toHaveBeenCalledTimes(2); + }); + + it('single read replica findFirst + primary findFirst', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); + + db.query.usersTable.findFirst(); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + + db.$primary.query.usersTable.findFirst(); + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(1); + }); + + it('always first read findFirst', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + const read2 = drizzle({} as any, { schema: { usersTable } }); + + const db = withReplicas(primaryDb, [read1, read2], (replicas) => { + return replicas[0]!; + }); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); + const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + + db.query.usersTable.findFirst(); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + + db.query.usersTable.findFirst(); + expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead2).toHaveBeenCalledTimes(0); + }); +}); + +describe('[findMany] read replicas singlestore', () => { + it('primary findMany', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + const read2 = drizzle({} as any, { schema: { usersTable } }); + + const db = withReplicas(primaryDb, [read1, read2]); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj = {} as any; + + const query = db.$primary.query.usersTable.findMany(obj); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(0); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj); + expect(query.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + }); + + it('random replica findMany', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + const read2 = drizzle({} as any, { schema: { usersTable } }); + + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); + + const db = withReplicas(primaryDb, [read1, read2], () => { + return randomMockReplica(); + }); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; + + const query1 = db.query.usersTable.findMany(obj1); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + expect(spyRead1).toHaveBeenCalledWith(obj1); + + const query2 = db.query.usersTable.findMany(obj2); + + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + expect(spyRead2).toHaveBeenCalledWith(obj2); + }); + + it('single read replica findMany', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; + + const query1 = db.query.usersTable.findMany(obj1); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + + const query2 = db.query.usersTable.findMany(obj2); + expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenNthCalledWith(2, obj2); + expect(query2.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + }); + + it('single read replica findMany + primary findMany', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + + const db = withReplicas(primaryDb, [read1]); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; + + const query1 = db.query.usersTable.findMany(obj1); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + + const query2 = db.$primary.query.usersTable.findMany(obj2); + + expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyPrimary).toHaveBeenNthCalledWith(1, obj2); + expect(query2.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + }); + + it('always first read findMany', () => { + const primaryDb = drizzle({} as any, { schema: { usersTable } }); + const read1 = drizzle({} as any, { schema: { usersTable } }); + const read2 = drizzle({} as any, { schema: { usersTable } }); + + const db = withReplicas(primaryDb, [read1, read2], (replicas) => { + return replicas[0]!; + }); + + const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); + const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; + + const query1 = db.query.usersTable.findMany(obj1); + + expect(spyPrimary).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + + const query2 = db.query.usersTable.findMany(obj2); + expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenNthCalledWith(2, obj2); + expect(query2.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + }); +}); diff --git a/integration-tests/tests/singlestore/singlestore-common.ts b/integration-tests/tests/singlestore/singlestore-common.ts new file mode 100644 index 000000000..037c27202 --- /dev/null +++ b/integration-tests/tests/singlestore/singlestore-common.ts @@ -0,0 +1,3432 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import 'dotenv/config'; +import Docker from 'dockerode'; +import { + and, + asc, + avg, + avgDistinct, + count, + countDistinct, + eq, + exists, + getTableColumns, + gt, + gte, + inArray, + lt, + max, + min, + Name, + notInArray, + placeholder, + sql, + sum, + sumDistinct, + TransactionRollbackError, +} from 'drizzle-orm'; +import type { SingleStoreDatabase } from 'drizzle-orm/singlestore-core'; +import { + alias, + bigint, + boolean, + date, + datetime, + decimal, + except, + getTableConfig, + getViewConfig, + int, + intersect, + json, + mediumint, + primaryKey, + serial, + singlestoreEnum, + singlestoreSchema, + singlestoreTable, + singlestoreTableCreator, + singlestoreView, + smallint, + text, + time, + timestamp, + tinyint, + union, + unionAll, + unique, + uniqueIndex, + uniqueKeyName, + varchar, + year, +} from 'drizzle-orm/singlestore-core'; +import { migrate } from 'drizzle-orm/singlestore/migrator'; +import getPort from 'get-port'; +import { v4 as uuid } from 'uuid'; +import { afterAll, beforeEach, describe, expect, expectTypeOf, test } from 'vitest'; +import { Expect, toLocalDate } from '~/utils.ts'; +import type { Equal } from '~/utils.ts'; + +type TestSingleStoreDB = SingleStoreDatabase; + +declare module 'vitest' { + interface TestContext { + singlestore: { + db: TestSingleStoreDB; + }; + } +} + +const ENABLE_LOGGING = false; + +const usersTable = singlestoreTable('userstest', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + verified: boolean('verified').notNull().default(false), + jsonb: json('jsonb').$type(), + createdAt: timestamp('created_at', { fsp: 6 }).notNull().defaultNow(), +}); + +const users2Table = singlestoreTable('users2', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id'), +}); + +const citiesTable = singlestoreTable('cities', { + id: serial('id').primaryKey(), + name: text('name').notNull(), +}); + +const usersOnUpdate = singlestoreTable('users_on_update', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + updateCounter: int('update_counter').default(sql`1`).$onUpdateFn(() => sql`update_counter + 1`), + updatedAt: datetime('updated_at', { mode: 'date', fsp: 6 }).$onUpdateFn(() => new Date()), + alwaysNull: text('always_null').$type().$onUpdateFn(() => null), // need to add $type because $onUpdate add a default value +}); + +const datesTable = singlestoreTable('datestable', { + date: date('date'), + dateAsString: date('date_as_string', { mode: 'string' }), + time: time('time', { fsp: 1 }), + datetime: datetime('datetime', { fsp: 6 }), + datetimeAsString: datetime('datetime_as_string', { fsp: 6, mode: 'string' }), + timestamp: timestamp('timestamp', { fsp: 6 }), + timestampAsString: timestamp('timestamp_as_string', { fsp: 6, mode: 'string' }), + year: year('year'), +}); + +const coursesTable = singlestoreTable('courses', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + categoryId: int('category_id'), +}); + +const courseCategoriesTable = singlestoreTable('course_categories', { + id: serial('id').primaryKey(), + name: text('name').notNull(), +}); + +const orders = singlestoreTable('orders', { + id: serial('id').primaryKey(), + region: text('region').notNull(), + product: text('product').notNull().$default(() => 'random_string'), + amount: int('amount').notNull(), + quantity: int('quantity').notNull(), +}); + +const usersMigratorTable = singlestoreTable('users12', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull(), +}, (table) => { + return { + name: uniqueIndex('').on(table.name).using('btree'), + }; +}); + +// To test aggregate functions +const aggregateTable = singlestoreTable('aggregate_table', { + id: serial('id').notNull(), + name: text('name').notNull(), + a: int('a'), + b: int('b'), + c: int('c'), + nullOnly: int('null_only'), +}); + +// To test another schema and multischema +const mySchema = singlestoreSchema(`mySchema`); + +const usersMySchemaTable = mySchema.table('userstest', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + verified: boolean('verified').notNull().default(false), + jsonb: json('jsonb').$type(), + createdAt: timestamp('created_at', { fsp: 6 }).notNull().defaultNow(), +}); + +const users2MySchemaTable = mySchema.table('users2', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id'), +}); + +const citiesMySchemaTable = mySchema.table('cities', { + id: serial('id').primaryKey(), + name: text('name').notNull(), +}); + +let singlestoreContainer: Docker.Container; +export async function createDockerDB(): Promise<{ connectionString: string; container: Docker.Container }> { + const docker = new Docker(); + const port = await getPort({ port: 3306 }); + const image = 'ghcr.io/singlestore-labs/singlestoredb-dev:latest'; + + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve(err))) + ); + + singlestoreContainer = await docker.createContainer({ + Image: image, + Env: ['ROOT_PASSWORD=singlestore'], + name: `drizzle-integration-tests-${uuid()}`, + HostConfig: { + AutoRemove: true, + PortBindings: { + '3306/tcp': [{ HostPort: `${port}` }], + }, + }, + }); + + await singlestoreContainer.start(); + await new Promise((resolve) => setTimeout(resolve, 4000)); + + return { + connectionString: `singlestore://root:singlestore@localhost:${port}/`, + container: singlestoreContainer, + }; +} + +// Tests are slow so we keep track of the test number +let testRunNumber = 0; + +export function tests(driver?: string) { + describe('common', () => { + afterAll(async () => { + await singlestoreContainer?.stop().catch(console.error); + }); + + beforeEach(async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`drop table if exists userstest`); + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + + await db.execute(sql`drop schema if exists \`mySchema\``); + await db.execute(sql`create schema if not exists \`mySchema\``); + + await db.execute( + sql` + create table userstest ( + id serial primary key, + name text not null, + verified boolean not null default false, + jsonb json, + created_at timestamp not null default now() + ) + `, + ); + + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id int + ) + `, + ); + + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + + // mySchema + await db.execute( + sql` + create table \`mySchema\`.\`userstest\` ( + \`id\` serial primary key, + \`name\` text not null, + \`verified\` boolean not null default false, + \`jsonb\` json, + \`created_at\` timestamp not null default now() + ) + `, + ); + + await db.execute( + sql` + create table \`mySchema\`.\`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.execute( + sql` + create table \`mySchema\`.\`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int + ) + `, + ); + + testRunNumber += 1; + console.log(`Test number: ${testRunNumber}`); + }); + + async function setupReturningFunctionsTest(db: SingleStoreDatabase) { + await db.execute(sql`drop table if exists \`users_default_fn\``); + await db.execute( + sql` + create table \`users_default_fn\` ( + \`id\` varchar(256) primary key, + \`name\` text not null + ); + `, + ); + } + + async function setupSetOperationTest(db: TestSingleStoreDB) { + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + } + + async function setupAggregateFunctionsTest(db: TestSingleStoreDB) { + await db.execute(sql`drop table if exists \`aggregate_table\``); + await db.execute( + sql` + create table \`aggregate_table\` ( + \`id\` integer primary key auto_increment not null, + \`name\` text not null, + \`a\` integer, + \`b\` integer, + \`c\` integer, + \`null_only\` integer + ); + `, + ); + await db.insert(aggregateTable).values([ + { id: 1, name: 'value 1', a: 5, b: 10, c: 20 }, + { id: 2, name: 'value 1', a: 5, b: 20, c: 30 }, + { id: 3, name: 'value 2', a: 10, b: 50, c: 60 }, + { id: 4, name: 'value 3', a: 20, b: 20, c: null }, + { id: 5, name: 'value 4', a: null, b: 90, c: 120 }, + { id: 6, name: 'value 5', a: 80, b: 10, c: null }, + { id: 7, name: 'value 6', a: null, b: null, c: 150 }, + ]); + } + + test('table config: unsigned ints', async () => { + const unsignedInts = singlestoreTable('cities1', { + bigint: bigint('bigint', { mode: 'number', unsigned: true }), + int: int('int', { unsigned: true }), + smallint: smallint('smallint', { unsigned: true }), + mediumint: mediumint('mediumint', { unsigned: true }), + tinyint: tinyint('tinyint', { unsigned: true }), + }); + + const tableConfig = getTableConfig(unsignedInts); + + const bigintColumn = tableConfig.columns.find((c) => c.name === 'bigint')!; + const intColumn = tableConfig.columns.find((c) => c.name === 'int')!; + const smallintColumn = tableConfig.columns.find((c) => c.name === 'smallint')!; + const mediumintColumn = tableConfig.columns.find((c) => c.name === 'mediumint')!; + const tinyintColumn = tableConfig.columns.find((c) => c.name === 'tinyint')!; + + expect(bigintColumn.getSQLType()).toBe('bigint unsigned'); + expect(intColumn.getSQLType()).toBe('int unsigned'); + expect(smallintColumn.getSQLType()).toBe('smallint unsigned'); + expect(mediumintColumn.getSQLType()).toBe('mediumint unsigned'); + expect(tinyintColumn.getSQLType()).toBe('tinyint unsigned'); + }); + + test('table config: signed ints', async () => { + const unsignedInts = singlestoreTable('cities1', { + bigint: bigint('bigint', { mode: 'number' }), + int: int('int'), + smallint: smallint('smallint'), + mediumint: mediumint('mediumint'), + tinyint: tinyint('tinyint'), + }); + + const tableConfig = getTableConfig(unsignedInts); + + const bigintColumn = tableConfig.columns.find((c) => c.name === 'bigint')!; + const intColumn = tableConfig.columns.find((c) => c.name === 'int')!; + const smallintColumn = tableConfig.columns.find((c) => c.name === 'smallint')!; + const mediumintColumn = tableConfig.columns.find((c) => c.name === 'mediumint')!; + const tinyintColumn = tableConfig.columns.find((c) => c.name === 'tinyint')!; + + expect(bigintColumn.getSQLType()).toBe('bigint'); + expect(intColumn.getSQLType()).toBe('int'); + expect(smallintColumn.getSQLType()).toBe('smallint'); + expect(mediumintColumn.getSQLType()).toBe('mediumint'); + expect(tinyintColumn.getSQLType()).toBe('tinyint'); + }); + + test('table config: primary keys name', async () => { + const table = singlestoreTable('cities', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + state: text('state'), + }, (t) => ({ + f: primaryKey({ columns: [t.id, t.name], name: 'custom_pk' }), + })); + + const tableConfig = getTableConfig(table); + + expect(tableConfig.primaryKeys).toHaveLength(1); + expect(tableConfig.primaryKeys[0]!.getName()).toBe('custom_pk'); + }); + + test('table configs: unique third param', async () => { + const cities1Table = singlestoreTable('cities1', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + state: text('state'), + }, (t) => ({ + f: unique('custom_name').on(t.name, t.state), + f1: unique('custom_name1').on(t.name, t.state), + })); + + const tableConfig = getTableConfig(cities1Table); + + expect(tableConfig.uniqueConstraints).toHaveLength(2); + + expect(tableConfig.uniqueConstraints[0]?.name).toBe('custom_name'); + expect(tableConfig.uniqueConstraints[0]?.columns.map((t) => t.name)).toEqual(['name', 'state']); + + expect(tableConfig.uniqueConstraints[1]?.name).toBe('custom_name1'); + expect(tableConfig.uniqueConstraints[1]?.columns.map((t) => t.name)).toEqual(['name', 'state']); + }); + + test('table configs: unique in column', async () => { + const cities1Table = singlestoreTable('cities1', { + id: serial('id').primaryKey(), + name: text('name').notNull().unique(), + state: text('state').unique('custom'), + field: text('field').unique('custom_field'), + }); + + const tableConfig = getTableConfig(cities1Table); + + const columnName = tableConfig.columns.find((it) => it.name === 'name'); + expect(columnName?.uniqueName).toBe(uniqueKeyName(cities1Table, [columnName!.name])); + expect(columnName?.isUnique).toBeTruthy(); + + const columnState = tableConfig.columns.find((it) => it.name === 'state'); + expect(columnState?.uniqueName).toBe('custom'); + expect(columnState?.isUnique).toBeTruthy(); + + const columnField = tableConfig.columns.find((it) => it.name === 'field'); + expect(columnField?.uniqueName).toBe('custom_field'); + expect(columnField?.isUnique).toBeTruthy(); + }); + + test('select all fields', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const result = await db.select().from(usersTable); + + expect(result[0]!.createdAt).toBeInstanceOf(Date); + // not timezone based timestamp, thats why it should not work here + // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + }); + + test('select sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.select({ + name: sql`upper(${usersTable.name})`, + }).from(usersTable); + + expect(users).toEqual([{ name: 'JOHN' }]); + }); + + test('select typed sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.select({ + name: sql`upper(${usersTable.name})`, + }).from(usersTable); + + expect(users).toEqual([{ name: 'JOHN' }]); + }); + + test('select with empty array in inArray', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + const result = await db + .select({ + name: sql`upper(${usersTable.name})`, + }) + .from(usersTable) + .where(inArray(usersTable.id, [])) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([]); + }); + + test('select with empty array in notInArray', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + const result = await db + .select({ + name: sql`upper(${usersTable.name})`, + }) + .from(usersTable) + .where(notInArray(usersTable.id, [])) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'JOHN' }, { name: 'JANE' }, { name: 'JANE' }]); + }); + + test('select distinct', async (ctx) => { + const { db } = ctx.singlestore; + + const usersDistinctTable = singlestoreTable('users_distinct', { + id: int('id').notNull(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${usersDistinctTable}`); + await db.execute(sql`create table ${usersDistinctTable} (id int, name text)`); + + await db.insert(usersDistinctTable).values([ + { id: 1, name: 'John' }, + { id: 1, name: 'John' }, + { id: 2, name: 'John' }, + { id: 1, name: 'Jane' }, + ]); + const users = await db.selectDistinct().from(usersDistinctTable).orderBy( + usersDistinctTable.id, + usersDistinctTable.name, + ); + + await db.execute(sql`drop table ${usersDistinctTable}`); + + expect(users).toEqual([{ id: 1, name: 'Jane' }, { id: 1, name: 'John' }, { id: 2, name: 'John' }]); + }); + + test('insert returning sql', async (ctx) => { + const { db } = ctx.singlestore; + + const [result, _] = await db.insert(usersTable).values({ id: 1, name: 'John' }); + + expect(result.insertId).toBe(1); + }); + + test('delete returning sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(users[0].affectedRows).toBe(1); + }); + + test('update returning sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + expect(users[0].changedRows).toBe(1); + }); + + test('update with returning all fields', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + const users = await db.select().from(usersTable).where(eq(usersTable.id, 1)); + + expect(updatedUsers[0].changedRows).toBe(1); + + expect(users[0]!.createdAt).toBeInstanceOf(Date); + // not timezone based timestamp, thats why it should not work here + // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000); + expect(users).toEqual([{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); + }); + + test('update with returning partial', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + const users = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(updatedUsers[0].changedRows).toBe(1); + + expect(users).toEqual([{ id: 1, name: 'Jane' }]); + }); + + test('delete with returning all fields', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(deletedUser[0].affectedRows).toBe(1); + }); + + test('delete with returning partial', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(deletedUser[0].affectedRows).toBe(1); + }); + + test('insert + select', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const result = await db.select().from(usersTable); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + + await db.insert(usersTable).values({ id: 2, name: 'Jane' }); + const result2 = await db.select().from(usersTable).orderBy(asc(usersTable.id)); + expect(result2).toEqual([ + { id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt }, + { id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt }, + ]); + }); + + test('json insert', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John', jsonb: ['foo', 'bar'] }); + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + jsonb: usersTable.jsonb, + }).from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]); + }); + + test('insert with overridden default values', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John', verified: true }); + const result = await db.select().from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'John', verified: true, jsonb: null, createdAt: result[0]!.createdAt }]); + }); + + test('insert many', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([ + { id: 1, name: 'John' }, + { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'] }, + { id: 3, name: 'Jane' }, + { id: 4, name: 'Austin', verified: true }, + ]); + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + jsonb: usersTable.jsonb, + verified: usersTable.verified, + }).from(usersTable) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([ + { id: 1, name: 'John', jsonb: null, verified: false }, + { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'], verified: false }, + { id: 3, name: 'Jane', jsonb: null, verified: false }, + { id: 4, name: 'Austin', jsonb: null, verified: true }, + ]); + }); + + test('insert many with returning', async (ctx) => { + const { db } = ctx.singlestore; + + const result = await db.insert(usersTable).values([ + { name: 'John' }, + { name: 'Bruce', jsonb: ['foo', 'bar'] }, + { name: 'Jane' }, + { name: 'Austin', verified: true }, + ]); + + expect(result[0].affectedRows).toBe(4); + }); + + test('select with group by as field', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.name) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]); + }); + + test('select with exists', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const user = alias(usersTable, 'user'); + const result = await db.select({ name: usersTable.name }).from(usersTable).where( + exists( + db.select({ one: sql`1` }).from(user).where(and(eq(usersTable.name, 'John'), eq(user.id, usersTable.id))), + ), + ) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }]); + }); + + test('select with group by as sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(sql`${usersTable.name}`) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]); + }); + + test('$default function', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`orders\``); + await db.execute( + sql` + create table \`orders\` ( + \`id\` serial primary key, + \`region\` text not null, + \`product\` text not null, + \`amount\` int not null, + \`quantity\` int not null + ) + `, + ); + + await db.insert(orders).values({ id: 1, region: 'Ukraine', amount: 1, quantity: 1 }); + const selectedOrder = await db.select().from(orders); + + expect(selectedOrder).toEqual([{ + id: 1, + amount: 1, + quantity: 1, + region: 'Ukraine', + product: 'random_string', + }]); + }); + + test('$default with empty array', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`s_orders\``); + await db.execute( + sql` + create table \`s_orders\` ( + \`id\` serial primary key, + \`region\` text default 'Ukraine', + \`product\` text not null + ) + `, + ); + + const users = singlestoreTable('s_orders', { + id: serial('id').primaryKey(), + region: text('region').default('Ukraine'), + product: text('product').$defaultFn(() => 'random_string'), + }); + + await db.insert(users).values({ id: 1 }); + const selectedOrder = await db.select().from(users); + + expect(selectedOrder).toEqual([{ + id: 1, + region: 'Ukraine', + product: 'random_string', + }]); + }); + + test('select with group by as sql + column', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(sql`${usersTable.name}`, usersTable.id) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); + }); + + test('select with group by as column + sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, sql`${usersTable.name}`) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); + }); + + test('select with group by complex query', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, sql`${usersTable.name}`) + .orderBy(asc(usersTable.name)) + .limit(1); + + expect(result).toEqual([{ name: 'Jane' }]); + }); + + test('build query', async (ctx) => { + const { db } = ctx.singlestore; + + const query = db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, usersTable.name) + .toSQL(); + + expect(query).toEqual({ + sql: `select \`id\`, \`name\` from \`userstest\` group by \`userstest\`.\`id\`, \`userstest\`.\`name\``, + params: [], + }); + }); + + test('Query check: Insert all defaults in 1 row', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').default('Dan'), + state: text('state'), + }); + + const query = db + .insert(users) + .values({}) + .toSQL(); + + expect(query).toEqual({ + sql: 'insert into `users` (`id`, `name`, `state`) values (default, default, default)', + params: [], + }); + }); + + test('Query check: Insert all defaults in multiple rows', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').default('Dan'), + state: text('state').default('UA'), + }); + + const query = db + .insert(users) + .values([{}, {}]) + .toSQL(); + + expect(query).toEqual({ + sql: + 'insert into `users` (`id`, `name`, `state`) values (default, default, default), (default, default, default)', + params: [], + }); + }); + + test('Insert all defaults in 1 row', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('empty_insert_single', { + id: serial('id').primaryKey(), + name: text('name').default('Dan'), + state: text('state'), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table ${users} (id serial primary key, name text default 'Dan', state text)`, + ); + + await db.insert(users).values({ id: 1 }); + + const res = await db.select().from(users); + + expect(res).toEqual([{ id: 1, name: 'Dan', state: null }]); + }); + + test('Insert all defaults in multiple rows', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('empty_insert_multiple', { + id: serial('id').primaryKey(), + name: text('name').default('Dan'), + state: text('state'), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table ${users} (id serial primary key, name text default 'Dan', state text)`, + ); + + await db.insert(users).values([{ id: 1 }, { id: 2 }]); + + const res = await db.select().from(users).orderBy(asc(users.id)); + + expect(res).toEqual([{ id: 1, name: 'Dan', state: null }, { id: 2, name: 'Dan', state: null }]); + }); + + test('build query insert with onDuplicate', async (ctx) => { + const { db } = ctx.singlestore; + + const query = db.insert(usersTable) + .values({ id: 1, name: 'John', jsonb: ['foo', 'bar'] }) + .onDuplicateKeyUpdate({ set: { id: 1, name: 'John1' } }) + .toSQL(); + + expect(query).toEqual({ + sql: + 'insert into `userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (?, ?, default, ?, default) on duplicate key update `id` = ?, `name` = ?', + params: [1, 'John', '["foo","bar"]', 1, 'John1'], + }); + }); + + test('insert with onDuplicate', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }); + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }) + .onDuplicateKeyUpdate({ set: { name: 'John1' } }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(res).toEqual([{ id: 1, name: 'John1' }]); + }); + + test('insert conflict', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }); + + await expect((async () => { + db.insert(usersTable).values({ id: 1, name: 'John1' }); + })()).resolves.not.toThrowError(); + }); + + test('insert conflict with ignore', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }); + + await db.insert(usersTable) + .ignore() + .values({ id: 1, name: 'John1' }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(res).toEqual([{ id: 1, name: 'John' }]); + }); + + test('insert sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: sql`${'John'}` }); + const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); + expect(result).toEqual([{ id: 1, name: 'John' }]); + }); + + test('partial join with alias', async (ctx) => { + const { db } = ctx.singlestore; + const customerAlias = alias(usersTable, 'customer'); + + await db.insert(usersTable).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select({ + user: { + id: usersTable.id, + name: usersTable.name, + }, + customer: { + id: customerAlias.id, + name: customerAlias.name, + }, + }).from(usersTable) + .leftJoin(customerAlias, eq(customerAlias.id, 11)) + .where(eq(usersTable.id, 10)) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ + user: { id: 10, name: 'Ivan' }, + customer: { id: 11, name: 'Hans' }, + }]); + }); + + test('full join with alias', async (ctx) => { + const { db } = ctx.singlestore; + + const singlestoreTable = singlestoreTableCreator((name) => `prefixed_${name}`); + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial primary key, name text not null)`); + + const customers = alias(users, 'customer'); + + await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select().from(users) + .leftJoin(customers, eq(customers.id, 11)) + .where(eq(users.id, 10)) + .orderBy(asc(users.id)); + + expect(result).toEqual([{ + users: { + id: 10, + name: 'Ivan', + }, + customer: { + id: 11, + name: 'Hans', + }, + }]); + + await db.execute(sql`drop table ${users}`); + }); + + test('select from alias', async (ctx) => { + const { db } = ctx.singlestore; + + const singlestoreTable = singlestoreTableCreator((name) => `prefixed_${name}`); + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial primary key, name text not null)`); + + const user = alias(users, 'user'); + const customers = alias(users, 'customer'); + + await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select() + .from(user) + .leftJoin(customers, eq(customers.id, 11)) + .where(eq(user.id, 10)) + .orderBy(asc(user.id)); + + expect(result).toEqual([{ + user: { + id: 10, + name: 'Ivan', + }, + customer: { + id: 11, + name: 'Hans', + }, + }]); + + await db.execute(sql`drop table ${users}`); + }); + + test('insert with spaces', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: sql`'Jo h n'` }); + const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'Jo h n' }]); + }); + + test('prepared statement', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const statement = db.select({ + id: usersTable.id, + name: usersTable.name, + }).from(usersTable) + .prepare(); + const result = await statement.execute(); + + expect(result).toEqual([{ id: 1, name: 'John' }]); + }); + + test('insert: placeholders on columns with encoder', async (ctx) => { + const { db } = ctx.singlestore; + + const date = new Date('2024-08-07T15:30:00Z'); + + const statement = db.insert(usersTable).values({ + id: 1, + name: 'John', + createdAt: sql.placeholder('createdAt'), + }).prepare(); + + await statement.execute({ createdAt: date }); + + const result = await db + .select({ + id: usersTable.id, + createdAt: usersTable.createdAt, + }) + .from(usersTable); + + expect(result).toEqual([ + { id: 1, createdAt: date }, + ]); + }); + + test('prepared statement reuse', async (ctx) => { + const { db } = ctx.singlestore; + + const stmt = db.insert(usersTable).values({ + verified: true, + id: placeholder('id'), + name: placeholder('name'), + }).prepare(); + + for (let i = 0; i < 10; i++) { + await stmt.execute({ id: i + 1, name: `John ${i}` }); + } + + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + verified: usersTable.verified, + }).from(usersTable) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([ + { id: 1, name: 'John 0', verified: true }, + { id: 2, name: 'John 1', verified: true }, + { id: 3, name: 'John 2', verified: true }, + { id: 4, name: 'John 3', verified: true }, + { id: 5, name: 'John 4', verified: true }, + { id: 6, name: 'John 5', verified: true }, + { id: 7, name: 'John 6', verified: true }, + { id: 8, name: 'John 7', verified: true }, + { id: 9, name: 'John 8', verified: true }, + { id: 10, name: 'John 9', verified: true }, + ]); + }); + + test('prepared statement with placeholder in .where', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const stmt = db.select({ + id: usersTable.id, + name: usersTable.name, + }).from(usersTable) + .where(eq(usersTable.id, placeholder('id'))) + .prepare(); + const result = await stmt.execute({ id: 1 }); + + expect(result).toEqual([{ id: 1, name: 'John' }]); + }); + + test('migrator', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists cities_migration`); + await db.execute(sql`drop table if exists users_migration`); + await db.execute(sql`drop table if exists users12`); + await db.execute(sql`drop table if exists __drizzle_migrations`); + + await migrate(db, { migrationsFolder: './drizzle2/singlestore' }); + + await db.insert(usersMigratorTable).values({ id: 1, name: 'John', email: 'email' }); + + const result = await db.select().from(usersMigratorTable); + + expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]); + + await db.execute(sql`drop table cities_migration`); + await db.execute(sql`drop table users_migration`); + await db.execute(sql`drop table users12`); + await db.execute(sql`drop table __drizzle_migrations`); + }); + + test('insert via db.execute + select via db.execute', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute( + sql`insert into ${usersTable} (${new Name(usersTable.id.name)},${new Name( + usersTable.name.name, + )}) values (1,${'John'})`, + ); + + const result = await db.execute<{ id: number; name: string }>(sql`select id, name from ${usersTable}`); + expect(result[0]).toEqual([{ id: 1, name: 'John' }]); + }); + + test('insert via db.execute w/ query builder', async (ctx) => { + const { db } = ctx.singlestore; + + const inserted = await db.execute( + db.insert(usersTable).values({ id: 1, name: 'John' }), + ); + expect(inserted[0].affectedRows).toBe(1); + }); + + test('insert + select all possible dates', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`datestable\``); + await db.execute( + sql` + create table \`datestable\` ( + \`date\` date, + \`date_as_string\` date, + \`time\` time, + \`datetime\` datetime, + \`datetime_as_string\` datetime, + \`timestamp\` timestamp(6), + \`timestamp_as_string\` timestamp(6), + \`year\` year + ) + `, + ); + + const date = new Date('2022-11-11'); + const dateWithMilliseconds = new Date('2022-11-11 12:12:12.123'); + + await db.insert(datesTable).values({ + date: date, + dateAsString: '2022-11-11', + time: '12:12:12', + datetime: date, + year: 22, + datetimeAsString: '2022-11-11 12:12:12', + timestamp: dateWithMilliseconds, + timestampAsString: '2022-11-11 12:12:12.123', + }); + + const res = await db.select().from(datesTable); + + expect(res[0]?.date).toBeInstanceOf(Date); + expect(res[0]?.datetime).toBeInstanceOf(Date); + expect(typeof res[0]?.dateAsString).toBe('string'); + expect(typeof res[0]?.datetimeAsString).toBe('string'); + + expect(res).toEqual([{ + date: toLocalDate(new Date('2022-11-11')), + dateAsString: '2022-11-11', + time: '12:12:12', + datetime: new Date('2022-11-11'), + year: 2022, + datetimeAsString: '2022-11-11 12:12:12', + timestamp: new Date('2022-11-11 12:12:12.123'), + timestampAsString: '2022-11-11 12:12:12.123000', + }]); + + await db.execute(sql`drop table if exists \`datestable\``); + }); + + const tableWithEnums = singlestoreTable('enums_test_case', { + id: serial('id').primaryKey(), + enum1: singlestoreEnum('enum1', ['a', 'b', 'c']).notNull(), + enum2: singlestoreEnum('enum2', ['a', 'b', 'c']).default('a'), + enum3: singlestoreEnum('enum3', ['a', 'b', 'c']).notNull().default('b'), + }); + + test('SingleStore enum test case #1', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`enums_test_case\``); + + await db.execute(sql` + create table \`enums_test_case\` ( + \`id\` serial primary key, + \`enum1\` ENUM('a', 'b', 'c') not null, + \`enum2\` ENUM('a', 'b', 'c') default 'a', + \`enum3\` ENUM('a', 'b', 'c') not null default 'b' + ) + `); + + await db.insert(tableWithEnums).values([ + { id: 1, enum1: 'a', enum2: 'b', enum3: 'c' }, + { id: 2, enum1: 'a', enum3: 'c' }, + { id: 3, enum1: 'a' }, + ]); + + const res = await db.select().from(tableWithEnums).orderBy(asc(tableWithEnums.id)); + + await db.execute(sql`drop table \`enums_test_case\``); + + expect(res).toEqual([ + { id: 1, enum1: 'a', enum2: 'b', enum3: 'c' }, + { id: 2, enum1: 'a', enum2: 'a', enum3: 'c' }, + { id: 3, enum1: 'a', enum2: 'a', enum3: 'b' }, + ]); + }); + + test('left join (flat object fields)', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(citiesTable) + .values([{ id: 1, name: 'Paris' }, { id: 2, name: 'London' }]); + + await db.insert(users2Table).values([{ id: 1, name: 'John', cityId: 1 }, { id: 2, name: 'Jane' }]); + + const res = await db.select({ + userId: users2Table.id, + userName: users2Table.name, + cityId: citiesTable.id, + cityName: citiesTable.name, + }).from(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .orderBy(users2Table.id); + + expect(res).toEqual([ + { userId: 1, userName: 'John', cityId: 1, cityName: 'Paris' }, + { userId: 2, userName: 'Jane', cityId: null, cityName: null }, + ]); + }); + + test('left join (grouped fields)', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(citiesTable) + .values([{ id: 1, name: 'Paris' }, { id: 2, name: 'London' }]); + + await db.insert(users2Table).values([{ id: 1, name: 'John', cityId: 1 }, { id: 2, name: 'Jane' }]); + + const res = await db.select({ + id: users2Table.id, + user: { + name: users2Table.name, + nameUpper: sql`upper(${users2Table.name})`, + }, + city: { + id: citiesTable.id, + name: citiesTable.name, + nameUpper: sql`upper(${citiesTable.name})`, + }, + }).from(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .orderBy(asc(users2Table.id)); + + expect(res).toEqual([ + { + id: 1, + user: { name: 'John', nameUpper: 'JOHN' }, + city: { id: 1, name: 'Paris', nameUpper: 'PARIS' }, + }, + { + id: 2, + user: { name: 'Jane', nameUpper: 'JANE' }, + city: null, + }, + ]); + }); + + test('left join (all fields)', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(citiesTable) + .values([{ id: 1, name: 'Paris' }, { id: 2, name: 'London' }]); + + await db.insert(users2Table).values([{ id: 1, name: 'John', cityId: 1 }, { id: 2, name: 'Jane' }]); + + const res = await db.select().from(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .orderBy(asc(users2Table.id)); + + expect(res).toEqual([ + { + users2: { + id: 1, + name: 'John', + cityId: 1, + }, + cities: { + id: 1, + name: 'Paris', + }, + }, + { + users2: { + id: 2, + name: 'Jane', + cityId: null, + }, + cities: null, + }, + ]); + }); + + test('join subquery', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`courses\``); + await db.execute(sql`drop table if exists \`course_categories\``); + + await db.execute( + sql` + create table \`course_categories\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.execute( + sql` + create table \`courses\` ( + \`id\` serial primary key, + \`name\` text not null, + \`category_id\` int + ) + `, + ); + + await db.insert(courseCategoriesTable).values([ + { id: 1, name: 'Category 1' }, + { id: 2, name: 'Category 2' }, + { id: 3, name: 'Category 3' }, + { id: 4, name: 'Category 4' }, + ]); + + await db.insert(coursesTable).values([ + { id: 1, name: 'Development', categoryId: 2 }, + { id: 2, name: 'IT & Software', categoryId: 3 }, + { id: 3, name: 'Marketing', categoryId: 4 }, + { id: 4, name: 'Design', categoryId: 1 }, + ]); + + const sq2 = db + .select({ + categoryId: courseCategoriesTable.id, + category: courseCategoriesTable.name, + total: sql`count(${courseCategoriesTable.id})`, + }) + .from(courseCategoriesTable) + .groupBy(courseCategoriesTable.id, courseCategoriesTable.name) + .as('sq2'); + + const res = await db + .select({ + courseName: coursesTable.name, + categoryId: sq2.categoryId, + }) + .from(coursesTable) + .leftJoin(sq2, eq(coursesTable.categoryId, sq2.categoryId)) + .orderBy(coursesTable.name); + + expect(res).toEqual([ + { courseName: 'Design', categoryId: 1 }, + { courseName: 'Development', categoryId: 2 }, + { courseName: 'IT & Software', categoryId: 3 }, + { courseName: 'Marketing', categoryId: 4 }, + ]); + + await db.execute(sql`drop table if exists \`courses\``); + await db.execute(sql`drop table if exists \`course_categories\``); + }); + + test('with ... select', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`orders\``); + await db.execute( + sql` + create table \`orders\` ( + \`id\` serial primary key, + \`region\` text not null, + \`product\` text not null, + \`amount\` int not null, + \`quantity\` int not null + ) + `, + ); + + await db.insert(orders).values([ + { region: 'Europe', product: 'A', amount: 10, quantity: 1 }, + { region: 'Europe', product: 'A', amount: 20, quantity: 2 }, + { region: 'Europe', product: 'B', amount: 20, quantity: 2 }, + { region: 'Europe', product: 'B', amount: 30, quantity: 3 }, + { region: 'US', product: 'A', amount: 30, quantity: 3 }, + { region: 'US', product: 'A', amount: 40, quantity: 4 }, + { region: 'US', product: 'B', amount: 40, quantity: 4 }, + { region: 'US', product: 'B', amount: 50, quantity: 5 }, + ]); + + const regionalSales = db + .$with('regional_sales') + .as( + db + .select({ + region: orders.region, + totalSales: sql`sum(${orders.amount})`.as('total_sales'), + }) + .from(orders) + .groupBy(orders.region), + ); + + const topRegions = db + .$with('top_regions') + .as( + db + .select({ + region: regionalSales.region, + }) + .from(regionalSales) + .where( + gt( + regionalSales.totalSales, + db.select({ sales: sql`sum(${regionalSales.totalSales})/10` }).from(regionalSales), + ), + ), + ); + + const result = await db + .with(regionalSales, topRegions) + .select({ + region: orders.region, + product: orders.product, + productUnits: sql`cast(sum(${orders.quantity}) as unsigned)`, + productSales: sql`cast(sum(${orders.amount}) as unsigned)`, + }) + .from(orders) + .where(inArray(orders.region, db.select({ region: topRegions.region }).from(topRegions))) + .groupBy(orders.region, orders.product) + .orderBy(orders.region, orders.product); + + expect(result).toEqual([ + { + region: 'Europe', + product: 'A', + productUnits: 3, + productSales: 30, + }, + { + region: 'Europe', + product: 'B', + productUnits: 5, + productSales: 50, + }, + { + region: 'US', + product: 'A', + productUnits: 7, + productSales: 70, + }, + { + region: 'US', + product: 'B', + productUnits: 9, + productSales: 90, + }, + ]); + }); + + test('with ... update', async (ctx) => { + const { db } = ctx.singlestore; + + const products = singlestoreTable('products', { + id: serial('id').primaryKey(), + price: decimal('price', { + precision: 15, + scale: 2, + }).notNull(), + cheap: boolean('cheap').notNull().default(false), + }); + + await db.execute(sql`drop table if exists ${products}`); + await db.execute(sql` + create table ${products} ( + id serial primary key, + price decimal(15, 2) not null, + cheap boolean not null default false + ) + `); + + await db.insert(products).values([ + { id: 1, price: '10.99' }, + { id: 2, price: '25.85' }, + { id: 3, price: '32.99' }, + { id: 4, price: '2.50' }, + { id: 5, price: '4.59' }, + ]); + + const averagePrice = db + .$with('average_price') + .as( + db + .select({ + value: sql`avg(${products.price})`.as('value'), + }) + .from(products), + ); + + await db + .with(averagePrice) + .update(products) + .set({ + cheap: true, + }) + .where(lt(products.price, sql`(select * from ${averagePrice})`)); + + const result = await db + .select({ + id: products.id, + }) + .from(products) + .where(eq(products.cheap, true)) + .orderBy(asc(products.id)); + + expect(result).toEqual([ + { id: 1 }, + { id: 4 }, + { id: 5 }, + ]); + }); + + test('with ... delete', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`orders\``); + await db.execute( + sql` + create table \`orders\` ( + \`id\` serial primary key, + \`region\` text not null, + \`product\` text not null, + \`amount\` int not null, + \`quantity\` int not null + ) + `, + ); + + await db.insert(orders).values([ + { id: 1, region: 'Europe', product: 'A', amount: 10, quantity: 1 }, + { id: 2, region: 'Europe', product: 'A', amount: 20, quantity: 2 }, + { id: 3, region: 'Europe', product: 'B', amount: 20, quantity: 2 }, + { id: 4, region: 'Europe', product: 'B', amount: 30, quantity: 3 }, + { id: 5, region: 'US', product: 'A', amount: 30, quantity: 3 }, + { id: 6, region: 'US', product: 'A', amount: 40, quantity: 4 }, + { id: 7, region: 'US', product: 'B', amount: 40, quantity: 4 }, + { id: 8, region: 'US', product: 'B', amount: 50, quantity: 5 }, + ]); + + const averageAmount = db + .$with('average_amount') + .as( + db + .select({ + value: sql`avg(${orders.amount})`.as('value'), + }) + .from(orders), + ); + + await db + .with(averageAmount) + .delete(orders) + .where(gt(orders.amount, sql`(select * from ${averageAmount})`)); + + const result = await db + .select({ + id: orders.id, + }) + .from(orders) + .orderBy(asc(orders.id)); + + expect(result).toEqual([ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + ]); + }); + + test('select from subquery sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(users2Table).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]); + + const sq = db + .select({ name: sql`concat(${users2Table.name}, " modified")`.as('name') }) + .from(users2Table) + .orderBy(asc(users2Table.id)) + .as('sq'); + + const res = await db.select({ name: sq.name }).from(sq); + + expect(res).toEqual([{ name: 'John modified' }, { name: 'Jane modified' }]); + }); + + test('select a field without joining its table', (ctx) => { + const { db } = ctx.singlestore; + + expect(() => db.select({ name: users2Table.name }).from(usersTable).prepare()).toThrowError(); + }); + + test('select all fields from subquery without alias', (ctx) => { + const { db } = ctx.singlestore; + + const sq = db.$with('sq').as(db.select({ name: sql`upper(${users2Table.name})` }).from(users2Table)); + + expect(() => db.select().from(sq).prepare()).toThrowError(); + }); + + test('select count()', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }]); + + const res = await db.select({ count: sql`count(*)` }).from(usersTable); + + expect(res).toEqual([{ count: 2 }]); + }); + + test('select for ...', (ctx) => { + const { db } = ctx.singlestore; + + { + const query = db.select().from(users2Table).for('update').toSQL(); + expect(query.sql).toMatch(/ for update$/); + } + { + const query = db.select().from(users2Table).for('share', { skipLocked: true }).toSQL(); + expect(query.sql).toMatch(/ for share skip locked$/); + } + { + const query = db.select().from(users2Table).for('update', { noWait: true }).toSQL(); + expect(query.sql).toMatch(/ for update no wait$/); + } + }); + + test('having', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(citiesTable).values([{ id: 1, name: 'London' }, { id: 2, name: 'Paris' }, { + id: 3, + name: 'New York', + }]); + + await db.insert(users2Table).values([{ id: 1, name: 'John', cityId: 1 }, { id: 2, name: 'Jane', cityId: 1 }, { + id: 3, + name: 'Jack', + cityId: 2, + }]); + + const result = await db + .select({ + id: citiesTable.id, + name: sql`upper(${citiesTable.name})`.as('upper_name'), + usersCount: sql`count(${users2Table.id})`.as('users_count'), + }) + .from(citiesTable) + .leftJoin(users2Table, eq(users2Table.cityId, citiesTable.id)) + .where(({ name }) => sql`length(${name}) >= 3`) + .groupBy(citiesTable.id) + .having(({ usersCount }) => sql`${usersCount} > 0`) + .orderBy(({ name }) => name); + + expect(result).toEqual([ + { + id: 1, + name: 'LONDON', + usersCount: 2, + }, + { + id: 2, + name: 'PARIS', + usersCount: 1, + }, + ]); + }); + + test('view', async (ctx) => { + const { db } = ctx.singlestore; + + const newYorkers1 = singlestoreView('new_yorkers') + .as((qb) => qb.select().from(users2Table).where(eq(users2Table.cityId, 1))); + + const newYorkers2 = singlestoreView('new_yorkers', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }).as(sql`select * from ${users2Table} where ${eq(users2Table.cityId, 1)}`); + + const newYorkers3 = singlestoreView('new_yorkers', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }).existing(); + + await db.execute(sql`create view new_yorkers as ${getViewConfig(newYorkers1).query}`); + + await db.insert(citiesTable).values([{ id: 1, name: 'New York' }, { id: 2, name: 'Paris' }]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + { id: 3, name: 'Jack', cityId: 2 }, + ]); + + { + const result = await db.select().from(newYorkers1).orderBy(asc(newYorkers1.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select().from(newYorkers2).orderBy(asc(newYorkers2.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select().from(newYorkers3).orderBy(asc(newYorkers3.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select({ name: newYorkers1.name }).from(newYorkers1).orderBy(asc(newYorkers1.id)); + expect(result).toEqual([ + { name: 'John' }, + { name: 'Jane' }, + ]); + } + + await db.execute(sql`drop view ${newYorkers1}`); + }); + + test('select from raw sql', async (ctx) => { + const { db } = ctx.singlestore; + + const result = await db.select({ + id: sql`id`, + name: sql`name`, + }).from(sql`(select 1 as id, 'John' as name) as users`); + + Expect>; + + expect(result).toEqual([ + { id: 1, name: 'John' }, + ]); + }); + + test('select from raw sql with joins', async (ctx) => { + const { db } = ctx.singlestore; + + const result = await db + .select({ + id: sql`users.id`, + name: sql`users.name`, + userCity: sql`users.city`, + cityName: sql`cities.name`, + }) + .from(sql`(select 1 as id, 'John' as name, 'New York' as city) as users`) + .leftJoin(sql`(select 1 as id, 'Paris' as name) as cities`, sql`cities.id = users.id`); + + Expect>; + + expect(result).toEqual([ + { id: 1, name: 'John', userCity: 'New York', cityName: 'Paris' }, + ]); + }); + + test('join on aliased sql from select', async (ctx) => { + const { db } = ctx.singlestore; + + const result = await db + .select({ + userId: sql`users.id`.as('userId'), + name: sql`users.name`, + userCity: sql`users.city`, + cityId: sql`cities.id`.as('cityId'), + cityName: sql`cities.name`, + }) + .from(sql`(select 1 as id, 'John' as name, 'New York' as city) as users`) + .leftJoin(sql`(select 1 as id, 'Paris' as name) as cities`, (cols) => eq(cols.cityId, cols.userId)); + + Expect< + Equal<{ userId: number; name: string; userCity: string; cityId: number; cityName: string }[], typeof result> + >; + + expect(result).toEqual([ + { userId: 1, name: 'John', userCity: 'New York', cityId: 1, cityName: 'Paris' }, + ]); + }); + + test('join on aliased sql from with clause', async (ctx) => { + const { db } = ctx.singlestore; + + const users = db.$with('users').as( + db.select({ + id: sql`id`.as('userId'), + name: sql`name`.as('userName'), + city: sql`city`.as('city'), + }).from( + sql`(select 1 as id, 'John' as name, 'New York' as city) as users`, + ), + ); + + const cities = db.$with('cities').as( + db.select({ + id: sql`id`.as('cityId'), + name: sql`name`.as('cityName'), + }).from( + sql`(select 1 as id, 'Paris' as name) as cities`, + ), + ); + + const result = await db + .with(users, cities) + .select({ + userId: users.id, + name: users.name, + userCity: users.city, + cityId: cities.id, + cityName: cities.name, + }) + .from(users) + .leftJoin(cities, (cols) => eq(cols.cityId, cols.userId)); + + Expect< + Equal<{ userId: number; name: string; userCity: string; cityId: number; cityName: string }[], typeof result> + >; + + expect(result).toEqual([ + { userId: 1, name: 'John', userCity: 'New York', cityId: 1, cityName: 'Paris' }, + ]); + }); + + test('prefixed table', async (ctx) => { + const { db } = ctx.singlestore; + + const singlestoreTable = singlestoreTableCreator((name) => `myprefix_${name}`); + + const users = singlestoreTable('test_prefixed_table_with_unique_name', { + id: int('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table myprefix_test_prefixed_table_with_unique_name (id int not null primary key, name text not null)`, + ); + + await db.insert(users).values({ id: 1, name: 'John' }); + + const result = await db.select().from(users); + + expect(result).toEqual([{ id: 1, name: 'John' }]); + + await db.execute(sql`drop table ${users}`); + }); + + test('orderBy with aliased column', (ctx) => { + const { db } = ctx.singlestore; + + const query = db.select({ + test: sql`something`.as('test'), + }).from(users2Table).orderBy((fields) => fields.test).toSQL(); + + expect(query.sql).toBe('select something as `test` from `users2` order by `test`'); + }); + + test('timestamp timezone', async (ctx) => { + const { db } = ctx.singlestore; + + const date = new Date(Date.parse('2020-01-01T12:34:56+07:00')); + + await db.insert(usersTable).values({ id: 1, name: 'With default times' }); + await db.insert(usersTable).values({ + id: 2, + name: 'Without default times', + createdAt: date, + }); + const users = await db.select().from(usersTable).orderBy(asc(usersTable.id)); + + // check that the timestamps are set correctly for default times + expect(Math.abs(users[0]!.createdAt.getTime() - Date.now())).toBeLessThan(2000); + + // check that the timestamps are set correctly for non default times + expect(Math.abs(users[1]!.createdAt.getTime() - date.getTime())).toBeLessThan(2000); + }); + + test('transaction', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users_transactions', { + id: serial('id').primaryKey(), + balance: int('balance').notNull(), + }); + const products = singlestoreTable('products_transactions', { + id: serial('id').primaryKey(), + price: int('price').notNull(), + stock: int('stock').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`drop table if exists ${products}`); + + await db.execute(sql`create table users_transactions (id serial not null primary key, balance int not null)`); + await db.execute( + sql`create table products_transactions (id serial not null primary key, price int not null, stock int not null)`, + ); + + const [{ insertId: userId }] = await db.insert(users).values({ id: 1, balance: 100 }); + const user = await db.select().from(users).where(eq(users.id, userId)).then((rows) => rows[0]!); + const [{ insertId: productId }] = await db.insert(products).values({ id: 1, price: 10, stock: 10 }); + const product = await db.select().from(products).where(eq(products.id, productId)).then((rows) => rows[0]!); + + await db.transaction(async (tx) => { + await tx.update(users).set({ balance: user.balance - product.price }).where(eq(users.id, user.id)); + await tx.update(products).set({ stock: product.stock - 1 }).where(eq(products.id, product.id)); + }); + + const result = await db.select().from(users); + + expect(result).toEqual([{ id: 1, balance: 90 }]); + + await db.execute(sql`drop table ${users}`); + await db.execute(sql`drop table ${products}`); + }); + + test('transaction rollback', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users_transactions_rollback', { + id: serial('id').primaryKey(), + balance: int('balance').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table users_transactions_rollback (id serial not null primary key, balance int not null)`, + ); + + await expect((async () => { + await db.transaction(async (tx) => { + await tx.insert(users).values({ balance: 100 }); + tx.rollback(); + }); + })()).rejects.toThrowError(TransactionRollbackError); + + const result = await db.select().from(users); + + expect(result).toEqual([]); + + await db.execute(sql`drop table ${users}`); + }); + + test('join subquery with join', async (ctx) => { + const { db } = ctx.singlestore; + + const internalStaff = singlestoreTable('internal_staff', { + userId: int('user_id').notNull(), + }); + + const customUser = singlestoreTable('custom_user', { + id: int('id').notNull(), + }); + + const ticket = singlestoreTable('ticket', { + staffId: int('staff_id').notNull(), + }); + + await db.execute(sql`drop table if exists ${internalStaff}`); + await db.execute(sql`drop table if exists ${customUser}`); + await db.execute(sql`drop table if exists ${ticket}`); + + await db.execute(sql`create table internal_staff (user_id integer not null)`); + await db.execute(sql`create table custom_user (id integer not null)`); + await db.execute(sql`create table ticket (staff_id integer not null)`); + + await db.insert(internalStaff).values({ userId: 1 }); + await db.insert(customUser).values({ id: 1 }); + await db.insert(ticket).values({ staffId: 1 }); + + const subq = db + .select() + .from(internalStaff) + .leftJoin(customUser, eq(internalStaff.userId, customUser.id)) + .as('internal_staff'); + + const mainQuery = await db + .select() + .from(ticket) + .leftJoin(subq, eq(subq.internal_staff.userId, ticket.staffId)); + + expect(mainQuery).toEqual([{ + ticket: { staffId: 1 }, + internal_staff: { + internal_staff: { userId: 1 }, + custom_user: { id: 1 }, + }, + }]); + + await db.execute(sql`drop table ${internalStaff}`); + await db.execute(sql`drop table ${customUser}`); + await db.execute(sql`drop table ${ticket}`); + }); + + test('subquery with view', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users_subquery_view', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }); + + const newYorkers = singlestoreView('new_yorkers').as((qb) => qb.select().from(users).where(eq(users.cityId, 1))); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`drop view if exists ${newYorkers}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, name text not null, city_id integer not null)`, + ); + await db.execute(sql`create view ${newYorkers} as select * from ${users} where city_id = 1`); + + await db.insert(users).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 1 }, + { id: 4, name: 'Jill', cityId: 2 }, + ]); + + const sq = db.$with('sq').as(db.select().from(newYorkers)); + const result = await db.with(sq).select().from(sq).orderBy(asc(sq.id)); + + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 3, name: 'Jack', cityId: 1 }, + ]); + + await db.execute(sql`drop view ${newYorkers}`); + await db.execute(sql`drop table ${users}`); + }); + + test('join view as subquery', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users_join_view', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }); + + const newYorkers = singlestoreView('new_yorkers').as((qb) => qb.select().from(users).where(eq(users.cityId, 1))); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`drop view if exists ${newYorkers}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, name text not null, city_id integer not null)`, + ); + await db.execute(sql`create view ${newYorkers} as select * from ${users} where city_id = 1`); + + await db.insert(users).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 1 }, + { id: 4, name: 'Jill', cityId: 2 }, + ]); + + const sq = db.select().from(newYorkers).as('new_yorkers_sq'); + + const result = await db.select().from(users).leftJoin(sq, eq(users.id, sq.id)).orderBy(asc(users.id)); + + expect(result).toEqual([ + { + users_join_view: { id: 1, name: 'John', cityId: 1 }, + new_yorkers_sq: { id: 1, name: 'John', cityId: 1 }, + }, + { + users_join_view: { id: 2, name: 'Jane', cityId: 2 }, + new_yorkers_sq: null, + }, + { + users_join_view: { id: 3, name: 'Jack', cityId: 1 }, + new_yorkers_sq: { id: 3, name: 'Jack', cityId: 1 }, + }, + { + users_join_view: { id: 4, name: 'Jill', cityId: 2 }, + new_yorkers_sq: null, + }, + ]); + + await db.execute(sql`drop view ${newYorkers}`); + await db.execute(sql`drop table ${users}`); + }); + + test('select iterator', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users_iterator', { + id: serial('id').primaryKey(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial not null primary key)`); + + await db.insert(users).values([{ id: 1 }, { id: 2 }, { id: 3 }]); + + const iter = db.select().from(users) + .orderBy(asc(users.id)) + .iterator(); + + const result: typeof users.$inferSelect[] = []; + + for await (const row of iter) { + result.push(row); + } + + expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); + }); + + test('select iterator w/ prepared statement', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users_iterator', { + id: serial('id').primaryKey(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial not null primary key)`); + + await db.insert(users).values([{ id: 1 }, { id: 2 }, { id: 3 }]); + + const prepared = db.select().from(users) + .orderBy(asc(users.id)) + .prepare(); + const iter = prepared.iterator(); + const result: typeof users.$inferSelect[] = []; + + for await (const row of iter) { + result.push(row); + } + + expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); + }); + + test('insert undefined', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name'), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, name text)`, + ); + + await expect((async () => { + await db.insert(users).values({ name: undefined }); + })()).resolves.not.toThrowError(); + + await db.execute(sql`drop table ${users}`); + }); + + test('update undefined', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name'), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, name text)`, + ); + + await expect((async () => { + await db.update(users).set({ name: undefined }); + })()).rejects.toThrowError(); + + await expect((async () => { + await db.update(users).set({ id: 1, name: undefined }); + })()).resolves.not.toThrowError(); + + await db.execute(sql`drop table ${users}`); + }); + + test('utc config for datetime', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`datestable\``); + await db.execute( + sql` + create table \`datestable\` ( + \`datetime_utc\` datetime(6), + \`datetime\` datetime(6) + ) + `, + ); + const datesTable = singlestoreTable('datestable', { + datetimeUTC: datetime('datetime_utc', { fsp: 6, mode: 'date' }), + datetime: datetime('datetime', { fsp: 6 }), + }); + + const dateObj = new Date('2022-11-11'); + const dateUtc = new Date('2022-11-11T12:12:12.122Z'); + + await db.insert(datesTable).values({ + datetimeUTC: dateUtc, + datetime: dateObj, + }); + + const res = await db.select().from(datesTable); + + const [rawSelect] = await db.execute(sql`select \`datetime_utc\` from \`datestable\``); + const selectedRow = (rawSelect as unknown as [{ datetime_utc: string }])[0]; + + expect(selectedRow.datetime_utc).toBe('2022-11-11 12:12:12.122000'); + expect(new Date(selectedRow.datetime_utc.replace(' ', 'T') + 'Z')).toEqual(dateUtc); + + expect(res[0]?.datetime).toBeInstanceOf(Date); + expect(res[0]?.datetimeUTC).toBeInstanceOf(Date); + + expect(res).toEqual([{ + datetimeUTC: dateUtc, + datetime: new Date('2022-11-11'), + }]); + + await db.execute(sql`drop table if exists \`datestable\``); + }); + + // TODO (https://memsql.atlassian.net/browse/MCDB-63261) allow chaining limit and orderby in subquery + test('set operations (union) from query builder with subquery', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + const citiesQuery = db + .select({ + id: citiesTable.id, + name: citiesTable.name, + orderCol: sql`0`.as('orderCol'), + }) + .from(citiesTable); + + const usersQuery = db + .select({ + id: users2Table.id, + name: users2Table.name, + orderCol: sql`1`.as('orderCol'), + }) + .from(users2Table); + + const unionQuery = db + .select({ + id: sql`id`, + name: sql`name`, + }) + .from( + citiesQuery.union(usersQuery).as('combined'), + ) + .orderBy(sql`orderCol`, sql`id`) + .limit(8); + + const result = await unionQuery; + + expect(result).toHaveLength(8); + + expect(result).toEqual([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'Jack' }, + { id: 4, name: 'Peter' }, + { id: 5, name: 'Ben' }, + ]); + + // union should throw if selected fields are not in the same order + await expect((async () => { + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).union( + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table), + ); + })()).rejects.toThrowError(); + }); + + test('set operations (union) as function', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const result = await union( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + expect(result).toHaveLength(2); + + expect(result).toEqual([ + { id: 1, name: 'New York' }, + { id: 1, name: 'John' }, + ]); + + await expect((async () => { + union( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + })()).rejects.toThrowError(); + }); + + test('set operations (union all) from query builder', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const sq = db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).orderBy(asc(sql`id`)).limit(2).unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).orderBy(asc(sql`id`)).limit(2), + ).as('sq'); + + const result = await db.select().from(sq).orderBy(asc(sql`id`)).limit(3); + + expect(result).toHaveLength(3); + + expect(result).toEqual([ + { id: 1, name: 'New York' }, + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + ]); + + await expect((async () => { + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).limit(2).unionAll( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).limit(2), + ).orderBy(asc(sql`id`)); + })()).rejects.toThrowError(); + }); + + test('set operations (union all) as function', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const sq = unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).as('sq'); + + const result = await db.select().from(sq).limit(1); + + expect(result).toHaveLength(1); + + expect(result).toEqual([ + { id: 1, name: 'New York' }, + ]); + + await expect((async () => { + unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).limit(1); + })()).rejects.toThrowError(); + }); + + test('set operations (intersect) from query builder', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const sq = db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + ) + .as('sq'); + + const result = await db.select().from(sq).orderBy(asc(sql`id`)); + + expect(result).toHaveLength(2); + + expect(result).toEqual([ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await expect((async () => { + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + ); + })()).rejects.toThrowError(); + }); + + test('set operations (intersect) as function', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const sq = await intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).as('sq'); + + const result = await db.select().from(sq).limit(1); + + expect(result).toHaveLength(0); + + expect(result).toEqual([]); + + await expect((async () => { + intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).limit(1); + })()).rejects.toThrowError(); + }); + + test('set operations (except) from query builder', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const result = await db + .select() + .from(citiesTable).except( + db + .select() + .from(citiesTable).where(gt(citiesTable.id, 1)), + ); + + expect(result).toHaveLength(1); + + expect(result).toEqual([ + { id: 1, name: 'New York' }, + ]); + }); + + test('set operations (except) as function', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const sq = await except( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable), + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).as('sq'); + + const result = await db.select().from(sq).limit(3); + + expect(result).toHaveLength(2); + + expect(result).toEqual([ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await expect((async () => { + except( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable), + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).limit(3); + })()).rejects.toThrowError(); + }); + + test('set operations (mixed) from query builder', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const sq1 = unionAll( + db + .select() + .from(citiesTable).where(gt(citiesTable.id, 1)), + db.select().from(citiesTable).where(eq(citiesTable.id, 2)), + ).as('sq1'); + + const sq2 = await db.select().from(sq1).orderBy(asc(sql`id`)).as('sq2'); + + const sq3 = await db.select().from(sq2).limit(1).offset(1).as('sq3'); + + const result = await db + .select() + .from(citiesTable) + .except( + db + .select() + .from(sq3), + ); + + expect(result).toHaveLength(2); + + expect(result).toEqual([ + { id: 3, name: 'Tampa' }, + { id: 1, name: 'New York' }, + ]); + + await expect((async () => { + db + .select() + .from(citiesTable).except( + ({ unionAll }) => + unionAll( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + db.select().from(citiesTable).where(eq(citiesTable.id, 2)), + ), + ); + })()).rejects.toThrowError(); + }); + + test('set operations (mixed all) as function with subquery', async (ctx) => { + const { db } = ctx.singlestore; + + await setupSetOperationTest(db); + + const sq1 = except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 7)), + ).as('sq1'); + + const sq2 = await db.select().from(sq1).orderBy(asc(sql`id`)).as('sq2'); + + const sq3 = await db.select().from(sq2).limit(1).as('sq3'); + + const result = await union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db.select().from(sq3), + db + .select().from(citiesTable).where(gt(citiesTable.id, 1)), + ); + + expect(result).toHaveLength(4); + + // multiple results possible as a result of the filters >= 5 and ==7 because singlestore doesn't guarantee order + // dynamically validate results + const hasValidEntry = (entry: { id: number; name: string }) => { + if (entry.id === 1) return entry.name === 'John'; + if (entry.id > 1 && entry.id < 5) return entry.name === 'Tampa' || entry.name === 'London'; + if (entry.id >= 5 && entry.id !== 7) return true; // Accept any entry with id >= 5 and not 7 + return false; + }; + + for (const entry of result) { + expect(hasValidEntry(entry)).toBe(true); + } + + await expect((async () => { + union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 7)), + ).limit(1), + db + .select().from(citiesTable).where(gt(citiesTable.id, 1)), + ); + })()).rejects.toThrowError(); + }); + + test('aggregate function: count', async (ctx) => { + const { db } = ctx.singlestore; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: count() }).from(table); + const result2 = await db.select({ value: count(table.a) }).from(table); + const result3 = await db.select({ value: countDistinct(table.name) }).from(table); + + expect(result1[0]?.value).toBe(7); + expect(result2[0]?.value).toBe(5); + expect(result3[0]?.value).toBe(6); + }); + + test('aggregate function: avg', async (ctx) => { + const { db } = ctx.singlestore; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: avg(table.b) }).from(table); + const result2 = await db.select({ value: avg(table.nullOnly) }).from(table); + const result3 = await db.select({ value: avgDistinct(table.b) }).from(table); + + expect(result1[0]?.value).toBe('33.3333'); + expect(result2[0]?.value).toBe(null); + expect(result3[0]?.value).toBe('42.5000'); + }); + + test('aggregate function: sum', async (ctx) => { + const { db } = ctx.singlestore; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: sum(table.b) }).from(table); + const result2 = await db.select({ value: sum(table.nullOnly) }).from(table); + const result3 = await db.select({ value: sumDistinct(table.b) }).from(table); + + expect(result1[0]?.value).toBe('200'); + expect(result2[0]?.value).toBe(null); + expect(result3[0]?.value).toBe('170'); + }); + + test('aggregate function: max', async (ctx) => { + const { db } = ctx.singlestore; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: max(table.b) }).from(table); + const result2 = await db.select({ value: max(table.nullOnly) }).from(table); + + expect(result1[0]?.value).toBe(90); + expect(result2[0]?.value).toBe(null); + }); + + test('aggregate function: min', async (ctx) => { + const { db } = ctx.singlestore; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: min(table.b) }).from(table); + const result2 = await db.select({ value: min(table.nullOnly) }).from(table); + + expect(result1[0]?.value).toBe(10); + expect(result2[0]?.value).toBe(null); + }); + + test('test $onUpdateFn and $onUpdate works as $default', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists ${usersOnUpdate}`); + + await db.execute( + sql` + create table ${usersOnUpdate} ( + id serial not null primary key, + name text not null, + update_counter integer default 1 not null, + updated_at datetime(6), + always_null text + ) + `, + ); + + await db.insert(usersOnUpdate).values([ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'Jack' }, + { id: 4, name: 'Jill' }, + ]); + const { updatedAt, ...rest } = getTableColumns(usersOnUpdate); + + const justDates = await db.select({ updatedAt }).from(usersOnUpdate); + + const response = await db.select({ ...rest }).from(usersOnUpdate).orderBy(asc(usersOnUpdate.id)); + + expect(response).toEqual([ + { name: 'John', id: 1, updateCounter: 1, alwaysNull: null }, + { name: 'Jane', id: 2, updateCounter: 1, alwaysNull: null }, + { name: 'Jack', id: 3, updateCounter: 1, alwaysNull: null }, + { name: 'Jill', id: 4, updateCounter: 1, alwaysNull: null }, + ]); + const msDelay = 750; + + for (const eachUser of justDates) { + expect(eachUser.updatedAt!.valueOf()).toBeGreaterThan(Date.now() - msDelay); + } + }); + + test('test $onUpdateFn and $onUpdate works updating', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists ${usersOnUpdate}`); + + await db.execute( + sql` + create table ${usersOnUpdate} ( + id serial not null primary key, + name text not null, + update_counter integer default 1 not null, + updated_at datetime(6), + always_null text + ) + `, + ); + + await db.insert(usersOnUpdate).values([ + { id: 1, name: 'John', alwaysNull: 'this will will be null after updating' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'Jack' }, + { id: 4, name: 'Jill' }, + ]); + const { updatedAt, ...rest } = getTableColumns(usersOnUpdate); + const initial = await db.select({ id: usersOnUpdate.id, updatedAt: usersOnUpdate.updatedAt }).from(usersOnUpdate); + + await db.update(usersOnUpdate).set({ name: 'Angel' }).where(eq(usersOnUpdate.id, 1)); + + const justDates = await db.select({ id: usersOnUpdate.id, updatedAt: usersOnUpdate.updatedAt }).from( + usersOnUpdate, + ); + + const response = await db.select().from(usersOnUpdate).orderBy(asc(usersOnUpdate.id)); + + expect(response).toEqual([ + { id: 1, name: 'Angel', updateCounter: 2, updatedAt: expect.any(Date), alwaysNull: null }, + { id: 2, name: 'Jane', updateCounter: 1, updatedAt: expect.any(Date), alwaysNull: null }, + { id: 3, name: 'Jack', updateCounter: 1, updatedAt: expect.any(Date), alwaysNull: null }, + { id: 4, name: 'Jill', updateCounter: 1, updatedAt: expect.any(Date), alwaysNull: null }, + ]); + + const initialRecord = initial.find((record) => record.id === 1); + const updatedRecord = justDates.find((record) => record.id === 1); + + expect(initialRecord?.updatedAt?.valueOf()).not.toBe(updatedRecord?.updatedAt?.valueOf()); + + const msDelay = 1000; + + for (const eachUser of justDates) { + expect(eachUser.updatedAt!.valueOf()).toBeGreaterThan(Date.now() - msDelay); + } + }); + + // mySchema tests + test('mySchema :: select all fields', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersMySchemaTable).values({ id: 1, name: 'John' }); + const result = await db.select().from(usersMySchemaTable); + + expect(result[0]!.createdAt).toBeInstanceOf(Date); + // not timezone based timestamp, thats why it should not work here + // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + }); + + test('mySchema :: select sql', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values({ name: 'John' }); + const users = await db.select({ + name: sql`upper(${usersMySchemaTable.name})`, + }).from(usersMySchemaTable); + + expect(users).toEqual([{ name: 'JOHN' }]); + }); + + test('mySchema :: select typed sql', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values({ name: 'John' }); + const users = await db.select({ + name: sql`upper(${usersMySchemaTable.name})`, + }).from(usersMySchemaTable); + + expect(users).toEqual([{ name: 'JOHN' }]); + }); + + test('mySchema :: select distinct', async (ctx) => { + const { db } = ctx.singlestore; + + const usersDistinctTable = singlestoreTable('users_distinct', { + id: int('id').notNull(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${usersDistinctTable}`); + await db.execute(sql`create table ${usersDistinctTable} (id int, name text)`); + + await db.insert(usersDistinctTable).values([ + { id: 1, name: 'John' }, + { id: 1, name: 'John' }, + { id: 2, name: 'John' }, + { id: 1, name: 'Jane' }, + ]); + const users = await db.selectDistinct().from(usersDistinctTable).orderBy( + usersDistinctTable.id, + usersDistinctTable.name, + ); + + await db.execute(sql`drop table ${usersDistinctTable}`); + + expect(users).toEqual([{ id: 1, name: 'Jane' }, { id: 1, name: 'John' }, { id: 2, name: 'John' }]); + }); + + test('mySchema :: insert returning sql', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + const [result, _] = await db.insert(usersMySchemaTable).values({ id: 1, name: 'John' }); + + expect(result.insertId).toBe(1); + }); + + test('mySchema :: delete returning sql', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values({ name: 'John' }); + const users = await db.delete(usersMySchemaTable).where(eq(usersMySchemaTable.name, 'John')); + + expect(users[0].affectedRows).toBe(1); + }); + + test('mySchema :: update with returning partial', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values({ id: 1, name: 'John' }); + const updatedUsers = await db.update(usersMySchemaTable).set({ name: 'Jane' }).where( + eq(usersMySchemaTable.name, 'John'), + ); + + const users = await db.select({ id: usersMySchemaTable.id, name: usersMySchemaTable.name }).from( + usersMySchemaTable, + ) + .where( + eq(usersMySchemaTable.id, 1), + ); + + expect(updatedUsers[0].changedRows).toBe(1); + + expect(users).toEqual([{ id: 1, name: 'Jane' }]); + }); + + test('mySchema :: delete with returning all fields', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersMySchemaTable).values({ name: 'John' }); + const deletedUser = await db.delete(usersMySchemaTable).where(eq(usersMySchemaTable.name, 'John')); + + expect(deletedUser[0].affectedRows).toBe(1); + }); + + test('mySchema :: insert + select', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values({ id: 1, name: 'John' }); + const result = await db.select().from(usersMySchemaTable); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + + await db.insert(usersMySchemaTable).values({ id: 2, name: 'Jane' }); + const result2 = await db.select().from(usersMySchemaTable).orderBy(asc(usersMySchemaTable.id)); + expect(result2).toEqual([ + { id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt }, + { id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt }, + ]); + }); + + test('mySchema :: insert with overridden default values', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values({ id: 1, name: 'John', verified: true }); + const result = await db.select().from(usersMySchemaTable); + + expect(result).toEqual([{ id: 1, name: 'John', verified: true, jsonb: null, createdAt: result[0]!.createdAt }]); + }); + + test('mySchema :: insert many', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values([ + { id: 1, name: 'John' }, + { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'] }, + { id: 3, name: 'Jane' }, + { id: 4, name: 'Austin', verified: true }, + ]); + const result = await db.select({ + id: usersMySchemaTable.id, + name: usersMySchemaTable.name, + jsonb: usersMySchemaTable.jsonb, + verified: usersMySchemaTable.verified, + }).from(usersMySchemaTable) + .orderBy(asc(usersMySchemaTable.id)); + + expect(result).toEqual([ + { id: 1, name: 'John', jsonb: null, verified: false }, + { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'], verified: false }, + { id: 3, name: 'Jane', jsonb: null, verified: false }, + { id: 4, name: 'Austin', jsonb: null, verified: true }, + ]); + }); + + test('mySchema :: select with group by as field', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { + id: 3, + name: 'Jane', + }]); + + const result = await db.select({ name: usersMySchemaTable.name }).from(usersMySchemaTable) + .groupBy(usersMySchemaTable.name) + .orderBy(asc(usersMySchemaTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]); + }); + + test('mySchema :: select with group by as column + sql', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { + id: 3, + name: 'Jane', + }]); + + const result = await db.select({ name: usersMySchemaTable.name }).from(usersMySchemaTable) + .groupBy(usersMySchemaTable.id, sql`${usersMySchemaTable.name}`) + .orderBy(asc(usersMySchemaTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); + }); + + test('mySchema :: build query', async (ctx) => { + const { db } = ctx.singlestore; + + const query = db.select({ id: usersMySchemaTable.id, name: usersMySchemaTable.name }).from(usersMySchemaTable) + .groupBy(usersMySchemaTable.id, usersMySchemaTable.name) + .toSQL(); + + expect(query).toEqual({ + sql: + `select \`id\`, \`name\` from \`mySchema\`.\`userstest\` group by \`userstest\`.\`id\`, \`userstest\`.\`name\``, + params: [], + }); + }); + + test('mySchema :: insert with spaces', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values({ id: 1, name: sql`'Jo h n'` }); + const result = await db.select({ id: usersMySchemaTable.id, name: usersMySchemaTable.name }).from( + usersMySchemaTable, + ); + + expect(result).toEqual([{ id: 1, name: 'Jo h n' }]); + }); + + test('mySchema :: prepared statement with placeholder in .where', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.insert(usersMySchemaTable).values({ id: 1, name: 'John' }); + const stmt = db.select({ + id: usersMySchemaTable.id, + name: usersMySchemaTable.name, + }).from(usersMySchemaTable) + .where(eq(usersMySchemaTable.id, sql.placeholder('id'))) + .prepare(); + const result = await stmt.execute({ id: 1 }); + + expect(result).toEqual([{ id: 1, name: 'John' }]); + }); + + test('mySchema :: select from tables with same name from different schema using alias', async (ctx) => { + const { db } = ctx.singlestore; + await db.execute(sql`truncate table \`mySchema\`.\`userstest\``); + + await db.execute(sql`drop table if exists \`userstest\``); + await db.execute( + sql` + create table \`userstest\` ( + \`id\` serial primary key, + \`name\` text not null, + \`verified\` boolean not null default false, + \`jsonb\` json, + \`created_at\` timestamp not null default now() + ) + `, + ); + + await db.insert(usersMySchemaTable).values({ id: 10, name: 'Ivan' }); + await db.insert(usersTable).values({ id: 11, name: 'Hans' }); + + const customerAlias = alias(usersTable, 'customer'); + + const result = await db + .select().from(usersMySchemaTable) + .leftJoin(customerAlias, eq(customerAlias.id, 11)) + .where(eq(usersMySchemaTable.id, 10)); + + expect(result).toEqual([{ + userstest: { + id: 10, + name: 'Ivan', + verified: false, + jsonb: null, + createdAt: result[0]!.userstest.createdAt, + }, + customer: { + id: 11, + name: 'Hans', + verified: false, + jsonb: null, + createdAt: result[0]!.customer!.createdAt, + }, + }]); + }); + + test('insert $returningId: serial as id', async (ctx) => { + const { db } = ctx.singlestore; + + const result = await db.insert(usersTable).values({ id: 1, name: 'John' }).$returningId(); + + expectTypeOf(result).toEqualTypeOf<{ + id: number; + }[]>(); + + expect(result).toStrictEqual([{ id: 1 }]); + }); + + test('insert $returningId: serial as id, batch insert', async (ctx) => { + const { db } = ctx.singlestore; + + const result = await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'John1' }]) + .$returningId(); + + expectTypeOf(result).toEqualTypeOf<{ + id: number; + }[]>(); + + // singlestore auto increments when batch inserting, so the ids increment by one + expect(result).toStrictEqual([{ id: 2 }, { id: 3 }]); + }); + + test('insert $returningId: $default as primary key', async (ctx) => { + const { db } = ctx.singlestore; + + const uniqueKeys = ['ao865jf3mcmkfkk8o5ri495z', 'dyqs529eom0iczo2efxzbcut']; + let iterator = 0; + + const usersTableDefFn = singlestoreTable('users_default_fn', { + customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(() => { + const value = uniqueKeys[iterator]!; + iterator++; + return value; + }), + name: text('name').notNull(), + }); + + await setupReturningFunctionsTest(db); + + const result = await db.insert(usersTableDefFn).values([{ name: 'John' }, { name: 'John1' }]) + // ^? + .$returningId(); + + expectTypeOf(result).toEqualTypeOf<{ + customId: string; + }[]>(); + + expect(result).toStrictEqual([{ customId: 'ao865jf3mcmkfkk8o5ri495z' }, { + customId: 'dyqs529eom0iczo2efxzbcut', + }]); + }); + + test('insert $returningId: $default as primary key with value', async (ctx) => { + const { db } = ctx.singlestore; + + const uniqueKeys = ['ao865jf3mcmkfkk8o5ri495z', 'dyqs529eom0iczo2efxzbcut']; + let iterator = 0; + + const usersTableDefFn = singlestoreTable('users_default_fn', { + customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(() => { + const value = uniqueKeys[iterator]!; + iterator++; + return value; + }), + name: text('name').notNull(), + }); + + await setupReturningFunctionsTest(db); + + const result = await db.insert(usersTableDefFn).values([{ name: 'John', customId: 'test' }, { name: 'John1' }]) + // ^? + .$returningId(); + + expectTypeOf(result).toEqualTypeOf<{ + customId: string; + }[]>(); + + expect(result).toStrictEqual([{ customId: 'test' }, { customId: 'ao865jf3mcmkfkk8o5ri495z' }]); + }); + + test('mySchema :: view', async (ctx) => { + const { db } = ctx.singlestore; + + const newYorkers1 = mySchema.view('new_yorkers') + .as((qb) => qb.select().from(users2MySchemaTable).where(eq(users2MySchemaTable.cityId, 1))); + + const newYorkers2 = mySchema.view('new_yorkers', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }).as(sql`select * from ${users2MySchemaTable} where ${eq(users2MySchemaTable.cityId, 1)}`); + + const newYorkers3 = mySchema.view('new_yorkers', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }).existing(); + + await db.execute(sql`create view ${newYorkers1} as ${getViewConfig(newYorkers1).query}`); + + await db.insert(citiesMySchemaTable).values([{ id: 1, name: 'New York' }, { id: 2, name: 'Paris' }]); + + await db.insert(users2MySchemaTable).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + { id: 3, name: 'Jack', cityId: 2 }, + ]); + + { + const result = await db.select().from(newYorkers1).orderBy(asc(newYorkers1.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select().from(newYorkers2).orderBy(asc(newYorkers2.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select().from(newYorkers3).orderBy(asc(newYorkers3.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select({ name: newYorkers1.name }).from(newYorkers1).orderBy(asc(newYorkers1.id)); + expect(result).toEqual([ + { name: 'John' }, + { name: 'Jane' }, + ]); + } + + await db.execute(sql`drop view ${newYorkers1}`); + }); + + test('limit 0', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db + .select() + .from(usersTable) + .limit(0); + + expect(users).toEqual([]); + }); + + test('limit -1', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db + .select() + .from(usersTable) + .limit(-1); + + expect(users.length).toBeGreaterThan(0); + }); + }); +} diff --git a/integration-tests/tests/singlestore/singlestore-custom.test.ts b/integration-tests/tests/singlestore/singlestore-custom.test.ts new file mode 100644 index 000000000..b05cd756b --- /dev/null +++ b/integration-tests/tests/singlestore/singlestore-custom.test.ts @@ -0,0 +1,827 @@ +import retry from 'async-retry'; +import type Docker from 'dockerode'; +import { asc, eq, Name, placeholder, sql } from 'drizzle-orm'; +import type { SingleStoreDriverDatabase } from 'drizzle-orm/singlestore'; +import { drizzle } from 'drizzle-orm/singlestore'; +import { + alias, + binary, + customType, + date, + datetime, + serial, + singlestoreEnum, + singlestoreTable, + singlestoreTableCreator, + text, + time, + varchar, + year, +} from 'drizzle-orm/singlestore-core'; +import { migrate } from 'drizzle-orm/singlestore/migrator'; +import * as mysql2 from 'mysql2/promise'; +import { v4 as uuid } from 'uuid'; +import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'; +import { toLocalDate } from '~/utils'; +import { createDockerDB } from './singlestore-common'; + +const ENABLE_LOGGING = false; + +let db: SingleStoreDriverDatabase; +let client: mysql2.Connection; +let container: Docker.Container | undefined; + +beforeAll(async () => { + let connectionString; + if (process.env['SINGLESTORE_CONNECTION_STRING']) { + connectionString = process.env['SINGLESTORE_CONNECTION_STRING']; + } else { + const { connectionString: conStr, container: contrainerObj } = await createDockerDB(); + connectionString = conStr; + container = contrainerObj; + } + client = await retry(async () => { + client = await mysql2.createConnection(connectionString); + await client.connect(); + return client; + }, { + retries: 20, + factor: 1, + minTimeout: 250, + maxTimeout: 250, + randomize: false, + onRetry() { + client?.end(); + }, + }); + await client.query(`CREATE DATABASE IF NOT EXISTS drizzle;`); + await client.changeUser({ database: 'drizzle' }); + db = drizzle(client, { logger: ENABLE_LOGGING }); +}); + +afterAll(async () => { + await client?.end(); + await container?.stop().catch(console.error); +}); + +beforeEach((ctx) => { + ctx.singlestore = { + db, + }; +}); + +const customSerial = customType<{ data: number; notNull: true; default: true }>({ + dataType() { + return 'serial'; + }, +}); + +const customText = customType<{ data: string }>({ + dataType() { + return 'text'; + }, +}); + +const customBoolean = customType<{ data: boolean }>({ + dataType() { + return 'boolean'; + }, + fromDriver(value) { + if (typeof value === 'boolean') { + return value; + } + return value === 1; + }, +}); + +const customJson = (name: string) => + customType<{ data: TData; driverData: string }>({ + dataType() { + return 'json'; + }, + toDriver(value: TData): string { + return JSON.stringify(value); + }, + })(name); + +const customTimestamp = customType< + { data: Date; driverData: string; config: { fsp: number } } +>({ + dataType(config) { + const precision = config?.fsp === undefined ? '' : ` (${config.fsp})`; + return `timestamp${precision}`; + }, + fromDriver(value: string): Date { + return new Date(value); + }, +}); + +const customBinary = customType<{ data: string; driverData: Buffer; config: { length: number } }>({ + dataType(config) { + return config?.length === undefined + ? `binary` + : `binary(${config.length})`; + }, + + toDriver(value) { + return sql`UNHEX(${value})`; + }, + + fromDriver(value) { + return value.toString('hex'); + }, +}); + +const usersTable = singlestoreTable('userstest', { + id: customSerial('id').primaryKey(), + name: customText('name').notNull(), + verified: customBoolean('verified').notNull().default(false), + jsonb: customJson('jsonb'), + createdAt: customTimestamp('created_at', { fsp: 6 }).notNull().default(sql`now()`), +}); + +const datesTable = singlestoreTable('datestable', { + date: date('date'), + dateAsString: date('date_as_string', { mode: 'string' }), + time: time('time', { fsp: 1 }), + datetime: datetime('datetime', { fsp: 6 }), + datetimeAsString: datetime('datetime_as_string', { fsp: 6, mode: 'string' }), + year: year('year'), +}); + +export const testTable = singlestoreTable('test_table', { + id: customBinary('id', { length: 16 }).primaryKey(), + sqlId: binary('sql_id', { length: 16 }), + rawId: varchar('raw_id', { length: 64 }), +}); + +const usersMigratorTable = singlestoreTable('users12', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull(), +}); + +beforeEach(async () => { + await db.execute(sql`drop table if exists \`userstest\``); + await db.execute(sql`drop table if exists \`datestable\``); + await db.execute(sql`drop table if exists \`test_table\``); + // await ctx.db.execute(sql`create schema public`); + await db.execute( + sql` + create table \`userstest\` ( + \`id\` serial primary key, + \`name\` text not null, + \`verified\` boolean not null default false, + \`jsonb\` json, + \`created_at\` timestamp not null default now() + ) + `, + ); + + await db.execute( + sql` + create table \`datestable\` ( + \`date\` date, + \`date_as_string\` date, + \`time\` time, + \`datetime\` datetime, + \`datetime_as_string\` datetime, + \`year\` year + ) + `, + ); + + await db.execute( + sql` + create table \`test_table\` ( + \`id\` binary(16) primary key, + \`sql_id\` binary(16), + \`raw_id\` varchar(64) + ) + `, + ); +}); + +test('select all fields', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const result = await db.select().from(usersTable); + + expect(result[0]!.createdAt).toBeInstanceOf(Date); + // not timezone based timestamp, thats why it should not work here + // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); +}); + +test('select sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.select({ + name: sql`upper(${usersTable.name})`, + }).from(usersTable); + + expect(users).toEqual([{ name: 'JOHN' }]); +}); + +test('select typed sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.select({ + name: sql`upper(${usersTable.name})`, + }).from(usersTable); + + expect(users).toEqual([{ name: 'JOHN' }]); +}); + +test('insert returning sql', async (ctx) => { + const { db } = ctx.singlestore; + + const [result, _] = await db.insert(usersTable).values({ id: 1, name: 'John' }); + + expect(result.insertId).toBe(1); +}); + +test('delete returning sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(users[0].affectedRows).toBe(1); +}); + +test('update returning sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + expect(users[0].changedRows).toBe(1); +}); + +test('update with returning all fields', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + const users = await db.select().from(usersTable).where(eq(usersTable.id, 1)); + + expect(updatedUsers[0].changedRows).toBe(1); + + expect(users[0]!.createdAt).toBeInstanceOf(Date); + // not timezone based timestamp, thats why it should not work here + // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000); + expect(users).toEqual([{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); +}); + +test('update with returning partial', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + const users = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(updatedUsers[0].changedRows).toBe(1); + + expect(users).toEqual([{ id: 1, name: 'Jane' }]); +}); + +test('delete with returning all fields', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(deletedUser[0].affectedRows).toBe(1); +}); + +test('delete with returning partial', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ name: 'John' }); + const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(deletedUser[0].affectedRows).toBe(1); +}); + +test('insert + select', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const result = await db.select().from(usersTable); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + + await db.insert(usersTable).values({ id: 2, name: 'Jane' }); + const result2 = await db.select().from(usersTable).orderBy(asc(usersTable.id)); + expect(result2).toEqual([ + { id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt }, + { id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt }, + ]); +}); + +test('json insert', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John', jsonb: ['foo', 'bar'] }); + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + jsonb: usersTable.jsonb, + }).from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]); +}); + +test('insert with overridden default values', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John', verified: true }); + const result = await db.select().from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'John', verified: true, jsonb: null, createdAt: result[0]!.createdAt }]); +}); + +test('insert many', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([ + { id: 1, name: 'John' }, + { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'] }, + { id: 3, name: 'Jane' }, + { id: 4, name: 'Austin', verified: true }, + ]); + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + jsonb: usersTable.jsonb, + verified: usersTable.verified, + }).from(usersTable).orderBy(asc(usersTable.id)); + + expect(result).toEqual([ + { id: 1, name: 'John', jsonb: null, verified: false }, + { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'], verified: false }, + { id: 3, name: 'Jane', jsonb: null, verified: false }, + { id: 4, name: 'Austin', jsonb: null, verified: true }, + ]); +}); + +test('insert many with returning', async (ctx) => { + const { db } = ctx.singlestore; + + const result = await db.insert(usersTable).values([ + { name: 'John' }, + { name: 'Bruce', jsonb: ['foo', 'bar'] }, + { name: 'Jane' }, + { name: 'Austin', verified: true }, + ]); + + expect(result[0].affectedRows).toBe(4); +}); + +test('select with group by as field', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.name).orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]); +}); + +test('select with group by as sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(sql`${usersTable.name}`).orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]); +}); + +test('select with group by as sql + column', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(sql`${usersTable.name}`, usersTable.id).orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); +}); + +test('select with group by as column + sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, sql`${usersTable.name}`).orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); +}); + +test('select with group by complex query', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, sql`${usersTable.name}`) + .orderBy(asc(usersTable.name)) + .limit(1); + + expect(result).toEqual([{ name: 'Jane' }]); +}); + +test('build query', async (ctx) => { + const { db } = ctx.singlestore; + + const query = db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, usersTable.name) + .toSQL(); + + expect(query).toEqual({ + sql: `select \`id\`, \`name\` from \`userstest\` group by \`userstest\`.\`id\`, \`userstest\`.\`name\``, + params: [], + }); +}); + +test('build query insert with onDuplicate', async (ctx) => { + const { db } = ctx.singlestore; + + const query = db.insert(usersTable) + .values({ name: 'John', jsonb: ['foo', 'bar'] }) + .onDuplicateKeyUpdate({ set: { name: 'John1' } }) + .toSQL(); + + expect(query).toEqual({ + sql: + 'insert into `userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (default, ?, default, ?, default) on duplicate key update `name` = ?', + params: ['John', '["foo","bar"]', 'John1'], + }); +}); + +test('insert with onDuplicate', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }); + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }) + .onDuplicateKeyUpdate({ set: { name: 'John1' } }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(res).toEqual([{ id: 1, name: 'John1' }]); +}); + +test('insert conflict', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }); + + await expect((async () => { + db.insert(usersTable).values({ id: 1, name: 'John1' }); + })()).resolves.not.toThrowError(); +}); + +test('insert conflict with ignore', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }); + + await db.insert(usersTable) + .ignore() + .values({ id: 1, name: 'John1' }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(res).toEqual([{ id: 1, name: 'John' }]); +}); + +test('insert sql', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: sql`${'John'}` }); + const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); + expect(result).toEqual([{ id: 1, name: 'John' }]); +}); + +test('partial join with alias', async (ctx) => { + const { db } = ctx.singlestore; + const customerAlias = alias(usersTable, 'customer'); + + await db.insert(usersTable).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select({ + user: { + id: usersTable.id, + name: usersTable.name, + }, + customer: { + id: customerAlias.id, + name: customerAlias.name, + }, + }).from(usersTable) + .leftJoin(customerAlias, eq(customerAlias.id, 11)) + .where(eq(usersTable.id, 10)) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ + user: { id: 10, name: 'Ivan' }, + customer: { id: 11, name: 'Hans' }, + }]); +}); + +test('full join with alias', async (ctx) => { + const { db } = ctx.singlestore; + + const singlestoreTable = singlestoreTableCreator((name) => `prefixed_${name}`); + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial primary key, name text not null)`); + + const customers = alias(users, 'customer'); + + await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select().from(users) + .leftJoin(customers, eq(customers.id, 11)) + .where(eq(users.id, 10)) + .orderBy(asc(users.id)); + + expect(result).toEqual([{ + users: { + id: 10, + name: 'Ivan', + }, + customer: { + id: 11, + name: 'Hans', + }, + }]); + + await db.execute(sql`drop table ${users}`); +}); + +test('select from alias', async (ctx) => { + const { db } = ctx.singlestore; + + const singlestoreTable = singlestoreTableCreator((name) => `prefixed_${name}`); + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial primary key, name text not null)`); + + const user = alias(users, 'user'); + const customers = alias(users, 'customer'); + + await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select() + .from(user) + .leftJoin(customers, eq(customers.id, 11)) + .where(eq(user.id, 10)) + .orderBy(asc(user.id)); + + expect(result).toEqual([{ + user: { + id: 10, + name: 'Ivan', + }, + customer: { + id: 11, + name: 'Hans', + }, + }]); + + await db.execute(sql`drop table ${users}`); +}); + +test('insert with spaces', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: sql`'Jo h n'` }); + const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'Jo h n' }]); +}); + +test('prepared statement', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const statement = db.select({ + id: usersTable.id, + name: usersTable.name, + }).from(usersTable) + .prepare(); + const result = await statement.execute(); + + expect(result).toEqual([{ id: 1, name: 'John' }]); +}); + +test('prepared statement reuse', async (ctx) => { + const { db } = ctx.singlestore; + + const stmt = db.insert(usersTable).values({ + id: placeholder('id'), + verified: true, + name: placeholder('name'), + }).prepare(); + + for (let i = 0; i < 10; i++) { + await stmt.execute({ id: i + 1, name: `John ${i}` }); + } + + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + verified: usersTable.verified, + }).from(usersTable).orderBy(asc(usersTable.id)); + + expect(result).toEqual([ + { id: 1, name: 'John 0', verified: true }, + { id: 2, name: 'John 1', verified: true }, + { id: 3, name: 'John 2', verified: true }, + { id: 4, name: 'John 3', verified: true }, + { id: 5, name: 'John 4', verified: true }, + { id: 6, name: 'John 5', verified: true }, + { id: 7, name: 'John 6', verified: true }, + { id: 8, name: 'John 7', verified: true }, + { id: 9, name: 'John 8', verified: true }, + { id: 10, name: 'John 9', verified: true }, + ]); +}); + +test('prepared statement with placeholder in .where', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const stmt = db.select({ + id: usersTable.id, + name: usersTable.name, + }).from(usersTable) + .where(eq(usersTable.id, placeholder('id'))) + .prepare(); + const result = await stmt.execute({ id: 1 }); + + expect(result).toEqual([{ id: 1, name: 'John' }]); +}); + +test('migrator', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists cities_migration`); + await db.execute(sql`drop table if exists users_migration`); + await db.execute(sql`drop table if exists users12`); + await db.execute(sql`drop table if exists __drizzle_migrations`); + + await migrate(db, { migrationsFolder: './drizzle2/singlestore' }); + + await db.insert(usersMigratorTable).values({ id: 1, name: 'John', email: 'email' }); + + const result = await db.select().from(usersMigratorTable); + + expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]); + + await db.execute(sql`drop table cities_migration`); + await db.execute(sql`drop table users_migration`); + await db.execute(sql`drop table users12`); + await db.execute(sql`drop table __drizzle_migrations`); +}); + +test('insert via db.execute + select via db.execute', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute( + sql`insert into ${usersTable} (${new Name(usersTable.id.name)}, ${new Name( + usersTable.name.name, + )}) values (1,${'John'})`, + ); + + const result = await db.execute<{ id: number; name: string }>(sql`select id, name from ${usersTable}`); + expect(result[0]).toEqual([{ id: 1, name: 'John' }]); +}); + +test('insert via db.execute w/ query builder', async (ctx) => { + const { db } = ctx.singlestore; + + const inserted = await db.execute( + db.insert(usersTable).values({ name: 'John' }), + ); + expect(inserted[0].affectedRows).toBe(1); +}); + +test('insert + select all possible dates', async (ctx) => { + const { db } = ctx.singlestore; + + const date = new Date('2022-11-11'); + + await db.insert(datesTable).values({ + date: date, + dateAsString: '2022-11-11', + time: '12:12:12', + datetime: date, + year: 22, + datetimeAsString: '2022-11-11 12:12:12', + }); + + const res = await db.select().from(datesTable); + + expect(res[0]?.date).toBeInstanceOf(Date); + expect(res[0]?.datetime).toBeInstanceOf(Date); + expect(res[0]?.dateAsString).toBeTypeOf('string'); + expect(res[0]?.datetimeAsString).toBeTypeOf('string'); + + expect(res).toEqual([{ + date: toLocalDate(new Date('2022-11-11')), + dateAsString: '2022-11-11', + time: '12:12:12', + datetime: new Date('2022-11-11'), + year: 2022, + datetimeAsString: '2022-11-11 12:12:12', + }]); +}); + +const tableWithEnums = singlestoreTable('enums_test_case', { + id: serial('id').primaryKey(), + enum1: singlestoreEnum('enum1', ['a', 'b', 'c']).notNull(), + enum2: singlestoreEnum('enum2', ['a', 'b', 'c']).default('a'), + enum3: singlestoreEnum('enum3', ['a', 'b', 'c']).notNull().default('b'), +}); + +test('SingleStore enum test case #1', async (ctx) => { + const { db } = ctx.singlestore; + + await db.execute(sql`drop table if exists \`enums_test_case\``); + + await db.execute(sql` + create table \`enums_test_case\` ( + \`id\` serial primary key, + \`enum1\` ENUM('a', 'b', 'c') not null, + \`enum2\` ENUM('a', 'b', 'c') default 'a', + \`enum3\` ENUM('a', 'b', 'c') not null default 'b' + ) + `); + + await db.insert(tableWithEnums).values([ + { id: 1, enum1: 'a', enum2: 'b', enum3: 'c' }, + { id: 2, enum1: 'a', enum3: 'c' }, + { id: 3, enum1: 'a' }, + ]); + + const res = await db.select().from(tableWithEnums).orderBy(asc(tableWithEnums.id)); + + await db.execute(sql`drop table \`enums_test_case\``); + + expect(res).toEqual([ + { id: 1, enum1: 'a', enum2: 'b', enum3: 'c' }, + { id: 2, enum1: 'a', enum2: 'a', enum3: 'c' }, + { id: 3, enum1: 'a', enum2: 'a', enum3: 'b' }, + ]); +}); + +test('custom binary', async (ctx) => { + const { db } = ctx.singlestore; + + const id = uuid().replace(/-/g, ''); + await db.insert(testTable).values({ + id, + sqlId: sql`UNHEX(${id})`, + rawId: id, + }); + + const res = await db.select().from(testTable); + + expect(res).toEqual([{ + id, + sqlId: Buffer.from(id, 'hex'), + rawId: id, + }]); +}); diff --git a/integration-tests/tests/singlestore/singlestore-prefixed.test.ts b/integration-tests/tests/singlestore/singlestore-prefixed.test.ts new file mode 100644 index 000000000..224ad433d --- /dev/null +++ b/integration-tests/tests/singlestore/singlestore-prefixed.test.ts @@ -0,0 +1,1572 @@ +import retry from 'async-retry'; +import type Docker from 'dockerode'; +import type { Equal } from 'drizzle-orm'; +import { asc, eq, getTableName, gt, inArray, Name, sql, TransactionRollbackError } from 'drizzle-orm'; +import type { SingleStoreDriverDatabase } from 'drizzle-orm/singlestore'; +import { drizzle } from 'drizzle-orm/singlestore'; +import { + alias, + boolean, + date, + datetime, + getViewConfig, + int, + json, + serial, + singlestoreEnum, + singlestoreTable as singlestoreTableRaw, + singlestoreTableCreator, + singlestoreView, + text, + time, + timestamp, + uniqueIndex, + year, +} from 'drizzle-orm/singlestore-core'; +import { migrate } from 'drizzle-orm/singlestore/migrator'; +import * as mysql2 from 'mysql2/promise'; +import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'; +import { Expect, toLocalDate } from '~/utils'; +import { createDockerDB } from './singlestore-common'; + +const ENABLE_LOGGING = false; + +let db: SingleStoreDriverDatabase; +let client: mysql2.Connection; +let container: Docker.Container | undefined; + +beforeAll(async () => { + let connectionString; + if (process.env['SINGLESTORE_CONNECTION_STRING']) { + connectionString = process.env['SINGLESTORE_CONNECTION_STRING']; + } else { + const { connectionString: conStr, container: contrainerObj } = await createDockerDB(); + connectionString = conStr; + container = contrainerObj; + } + client = await retry(async () => { + client = await mysql2.createConnection(connectionString); + await client.connect(); + return client; + }, { + retries: 20, + factor: 1, + minTimeout: 250, + maxTimeout: 250, + randomize: false, + onRetry() { + client?.end(); + }, + }); + + await client.query(`CREATE DATABASE IF NOT EXISTS drizzle;`); + await client.changeUser({ database: 'drizzle' }); + db = drizzle(client, { logger: ENABLE_LOGGING }); +}); + +afterAll(async () => { + await client?.end(); + await container?.stop().catch(console.error); +}); + +const tablePrefix = 'drizzle_tests_'; + +const singlestoreTable = singlestoreTableCreator((name) => `${tablePrefix}${name}`); +const usersTable = singlestoreTable('userstest', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + verified: boolean('verified').notNull().default(false), + jsonb: json('jsonb').$type(), + createdAt: timestamp('created_at', { fsp: 6 }).notNull().defaultNow(), +}); + +const users2Table = singlestoreTable('users2', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id'), +}); + +const citiesTable = singlestoreTable('cities', { + id: serial('id').primaryKey(), + name: text('name').notNull(), +}); + +beforeEach(async () => { + await db.execute(sql`drop table if exists ${usersTable}`); + await db.execute(sql`drop table if exists ${users2Table}`); + await db.execute(sql`drop table if exists ${citiesTable}`); + + await db.execute( + sql` + create table ${usersTable} ( + \`id\` serial primary key, + \`name\` text not null, + \`verified\` boolean not null default false, + \`jsonb\` json, + \`created_at\` timestamp not null default now() + ) + `, + ); + + await db.execute( + sql` + create table ${users2Table} ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int + ) + `, + ); + + await db.execute( + sql` + create table ${citiesTable} ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); +}); + +test('select all fields', async () => { + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const result = await db.select().from(usersTable); + + expect(result[0]!.createdAt).toBeInstanceOf(Date); + // not timezone based timestamp, thats why it should not work here + // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); +}); + +test('select sql', async () => { + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.select({ + name: sql`upper(${usersTable.name})`, + }).from(usersTable); + + expect(users).toEqual([{ name: 'JOHN' }]); +}); + +test('select typed sql', async () => { + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.select({ + name: sql`upper(${usersTable.name})`, + }).from(usersTable); + + expect(users).toEqual([{ name: 'JOHN' }]); +}); + +test('select distinct', async () => { + const usersDistinctTable = singlestoreTable('users_distinct', { + id: int('id').notNull(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${usersDistinctTable}`); + await db.execute(sql`create table ${usersDistinctTable} (id int, name text)`); + + await db.insert(usersDistinctTable).values([ + { id: 1, name: 'John' }, + { id: 1, name: 'John' }, + { id: 2, name: 'John' }, + { id: 1, name: 'Jane' }, + ]); + const users = await db.selectDistinct().from(usersDistinctTable).orderBy( + usersDistinctTable.id, + usersDistinctTable.name, + ); + + await db.execute(sql`drop table ${usersDistinctTable}`); + + expect(users).toEqual([{ id: 1, name: 'Jane' }, { id: 1, name: 'John' }, { id: 2, name: 'John' }]); +}); + +test('insert returning sql', async () => { + const [result, _] = await db.insert(usersTable).values({ id: 1, name: 'John' }); + + expect(result.insertId).toBe(1); +}); + +test('delete returning sql', async () => { + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(users[0].affectedRows).toBe(1); +}); + +test('update returning sql', async () => { + await db.insert(usersTable).values({ name: 'John' }); + const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + expect(users[0].changedRows).toBe(1); +}); + +test('update with returning all fields', async () => { + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + const users = await db.select().from(usersTable).where(eq(usersTable.id, 1)); + + expect(updatedUsers[0].changedRows).toBe(1); + + expect(users[0]!.createdAt).toBeInstanceOf(Date); + // not timezone based timestamp, thats why it should not work here + // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000); + expect(users).toEqual([{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); +}); + +test('update with returning partial', async () => { + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); + + const users = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(updatedUsers[0].changedRows).toBe(1); + + expect(users).toEqual([{ id: 1, name: 'Jane' }]); +}); + +test('delete with returning all fields', async () => { + await db.insert(usersTable).values({ name: 'John' }); + const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(deletedUser[0].affectedRows).toBe(1); +}); + +test('delete with returning partial', async () => { + await db.insert(usersTable).values({ name: 'John' }); + const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John')); + + expect(deletedUser[0].affectedRows).toBe(1); +}); + +test('insert + select', async () => { + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const result = await db.select().from(usersTable); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + + await db.insert(usersTable).values({ id: 2, name: 'Jane' }); + const result2 = await db.select().from(usersTable).orderBy(asc(usersTable.id)); + expect(result2).toEqual([ + { id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt }, + { id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt }, + ]); +}); + +test('json insert', async () => { + await db.insert(usersTable).values({ id: 1, name: 'John', jsonb: ['foo', 'bar'] }); + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + jsonb: usersTable.jsonb, + }).from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]); +}); + +test('insert with overridden default values', async () => { + await db.insert(usersTable).values({ id: 1, name: 'John', verified: true }); + const result = await db.select().from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'John', verified: true, jsonb: null, createdAt: result[0]!.createdAt }]); +}); + +test('insert many', async () => { + await db.insert(usersTable).values([ + { id: 1, name: 'John' }, + { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'] }, + { id: 3, name: 'Jane' }, + { id: 4, name: 'Austin', verified: true }, + ]); + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + jsonb: usersTable.jsonb, + verified: usersTable.verified, + }).from(usersTable) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([ + { id: 1, name: 'John', jsonb: null, verified: false }, + { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'], verified: false }, + { id: 3, name: 'Jane', jsonb: null, verified: false }, + { id: 4, name: 'Austin', jsonb: null, verified: true }, + ]); +}); + +test('insert many with returning', async () => { + const result = await db.insert(usersTable).values([ + { name: 'John' }, + { name: 'Bruce', jsonb: ['foo', 'bar'] }, + { name: 'Jane' }, + { name: 'Austin', verified: true }, + ]); + + expect(result[0].affectedRows).toBe(4); +}); + +test('select with group by as field', async () => { + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.name) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]); +}); + +test('select with group by as sql', async () => { + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(sql`${usersTable.name}`) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]); +}); + +test('select with group by as sql + column', async () => { + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(sql`${usersTable.name}`, usersTable.id) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); +}); + +test('select with group by as column + sql', async () => { + await db.insert(usersTable).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, sql`${usersTable.name}`) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); +}); + +test('select with group by complex query', async () => { + await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); + + const result = await db.select({ name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, sql`${usersTable.name}`) + .orderBy(asc(usersTable.name)) + .limit(1); + + expect(result).toEqual([{ name: 'Jane' }]); +}); + +test('build query', async () => { + const query = db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable) + .groupBy(usersTable.id, usersTable.name) + .toSQL(); + + expect(query).toEqual({ + sql: `select \`id\`, \`name\` from \`${getTableName(usersTable)}\` group by \`${ + getTableName(usersTable) + }\`.\`id\`, \`${getTableName(usersTable)}\`.\`name\``, + params: [], + }); +}); + +test('build query insert with onDuplicate', async () => { + const query = db.insert(usersTable) + .values({ name: 'John', jsonb: ['foo', 'bar'] }) + .onDuplicateKeyUpdate({ set: { name: 'John1' } }) + .toSQL(); + + expect(query).toEqual({ + sql: `insert into \`${ + getTableName(usersTable) + }\` (\`id\`, \`name\`, \`verified\`, \`jsonb\`, \`created_at\`) values (default, ?, default, ?, default) on duplicate key update \`name\` = ?`, + params: ['John', '["foo","bar"]', 'John1'], + }); +}); + +test('insert with onDuplicate', async () => { + await db.insert(usersTable) + .values({ id: 1, name: 'John' }); + + await db.insert(usersTable) + .values({ id: 1, name: 'John' }) + .onDuplicateKeyUpdate({ set: { name: 'John1' } }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(res).toEqual([{ id: 1, name: 'John1' }]); +}); + +test('insert conflict', async () => { + await db.insert(usersTable) + .values({ name: 'John' }); + + await expect((async () => { + db.insert(usersTable).values({ id: 1, name: 'John1' }); + })()).resolves.not.toThrowError(); +}); + +test('insert conflict with ignore', async () => { + await db.insert(usersTable) + .values({ id: 1, name: 'John' }); + + await db.insert(usersTable) + .ignore() + .values({ id: 1, name: 'John1' }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + expect(res).toEqual([{ id: 1, name: 'John' }]); +}); + +test('insert sql', async () => { + await db.insert(usersTable).values({ id: 1, name: sql`${'John'}` }); + const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); + expect(result).toEqual([{ id: 1, name: 'John' }]); +}); + +test('partial join with alias', async () => { + const customerAlias = alias(usersTable, 'customer'); + + await db.insert(usersTable).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select({ + user: { + id: usersTable.id, + name: usersTable.name, + }, + customer: { + id: customerAlias.id, + name: customerAlias.name, + }, + }).from(usersTable) + .leftJoin(customerAlias, eq(customerAlias.id, 11)) + .where(eq(usersTable.id, 10)) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([{ + user: { id: 10, name: 'Ivan' }, + customer: { id: 11, name: 'Hans' }, + }]); +}); + +test('full join with alias', async () => { + const singlestoreTable = singlestoreTableCreator((name) => `prefixed_${name}`); + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial primary key, name text not null)`); + + const customers = alias(users, 'customer'); + + await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select().from(users) + .leftJoin(customers, eq(customers.id, 11)) + .where(eq(users.id, 10)) + .orderBy(asc(users.id)); + + expect(result).toEqual([{ + users: { + id: 10, + name: 'Ivan', + }, + customer: { + id: 11, + name: 'Hans', + }, + }]); + + await db.execute(sql`drop table ${users}`); +}); + +test('select from alias', async () => { + const singlestoreTable = singlestoreTableCreator((name) => `prefixed_${name}`); + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial primary key, name text not null)`); + + const user = alias(users, 'user'); + const customers = alias(users, 'customer'); + + await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); + const result = await db + .select() + .from(user) + .leftJoin(customers, eq(customers.id, 11)) + .where(eq(user.id, 10)) + .orderBy(asc(user.id)); + + expect(result).toEqual([{ + user: { + id: 10, + name: 'Ivan', + }, + customer: { + id: 11, + name: 'Hans', + }, + }]); + + await db.execute(sql`drop table ${users}`); +}); + +test('insert with spaces', async () => { + await db.insert(usersTable).values({ id: 1, name: sql`'Jo h n'` }); + const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); + + expect(result).toEqual([{ id: 1, name: 'Jo h n' }]); +}); + +test('prepared statement', async () => { + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const statement = db.select({ + id: usersTable.id, + name: usersTable.name, + }).from(usersTable) + .prepare(); + const result = await statement.execute(); + + expect(result).toEqual([{ id: 1, name: 'John' }]); +}); + +test('prepared statement reuse', async () => { + const stmt = db.insert(usersTable).values({ + verified: true, + id: sql.placeholder('id'), + name: sql.placeholder('name'), + }).prepare(); + + for (let i = 0; i < 10; i++) { + await stmt.execute({ id: i + 1, name: `John ${i}` }); + } + + const result = await db.select({ + id: usersTable.id, + name: usersTable.name, + verified: usersTable.verified, + }).from(usersTable) + .orderBy(asc(usersTable.id)); + + expect(result).toEqual([ + { id: 1, name: 'John 0', verified: true }, + { id: 2, name: 'John 1', verified: true }, + { id: 3, name: 'John 2', verified: true }, + { id: 4, name: 'John 3', verified: true }, + { id: 5, name: 'John 4', verified: true }, + { id: 6, name: 'John 5', verified: true }, + { id: 7, name: 'John 6', verified: true }, + { id: 8, name: 'John 7', verified: true }, + { id: 9, name: 'John 8', verified: true }, + { id: 10, name: 'John 9', verified: true }, + ]); +}); + +test('prepared statement with placeholder in .where', async () => { + await db.insert(usersTable).values({ id: 1, name: 'John' }); + const stmt = db.select({ + id: usersTable.id, + name: usersTable.name, + }).from(usersTable) + .where(eq(usersTable.id, sql.placeholder('id'))) + .prepare(); + const result = await stmt.execute({ id: 1 }); + + expect(result).toEqual([{ id: 1, name: 'John' }]); +}); + +test('migrator', async () => { + const usersMigratorTable = singlestoreTableRaw('users12', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull(), + }, (table) => { + return { + name: uniqueIndex('').on(table.name).using('btree'), + }; + }); + + await db.execute(sql.raw(`drop table if exists cities_migration`)); + await db.execute(sql.raw(`drop table if exists users_migration`)); + await db.execute(sql.raw(`drop table if exists users12`)); + await db.execute(sql.raw(`drop table if exists __drizzle_migrations`)); + + await migrate(db, { migrationsFolder: './drizzle2/singlestore' }); + + await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); + + const result = await db.select().from(usersMigratorTable); + + expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]); + + await db.execute(sql.raw(`drop table cities_migration`)); + await db.execute(sql.raw(`drop table users_migration`)); + await db.execute(sql.raw(`drop table users12`)); + await db.execute(sql.raw(`drop table __drizzle_migrations`)); +}); + +test('insert via db.execute + select via db.execute', async () => { + await db.execute( + sql`insert into ${usersTable} (${new Name(usersTable.id.name)}, ${new Name( + usersTable.name.name, + )}) values (1, ${'John'})`, + ); + + const result = await db.execute<{ id: number; name: string }>(sql`select id, name from ${usersTable}`); + expect(result[0]).toEqual([{ id: 1, name: 'John' }]); +}); + +test('insert via db.execute w/ query builder', async () => { + const inserted = await db.execute( + db.insert(usersTable).values({ name: 'John' }), + ); + expect(inserted[0].affectedRows).toBe(1); +}); + +test('insert + select all possible dates', async () => { + const datesTable = singlestoreTable('datestable', { + date: date('date'), + dateAsString: date('date_as_string', { mode: 'string' }), + time: time('time', { fsp: 1 }), + datetime: datetime('datetime', { fsp: 6 }), + datetimeAsString: datetime('datetime_as_string', { fsp: 6, mode: 'string' }), + year: year('year'), + }); + + await db.execute(sql`drop table if exists ${datesTable}`); + await db.execute( + sql` + create table ${datesTable} ( + \`date\` date, + \`date_as_string\` date, + \`time\` time, + \`datetime\` datetime, + \`datetime_as_string\` datetime, + \`year\` year + ) + `, + ); + + const d = new Date('2022-11-11'); + + await db.insert(datesTable).values({ + date: d, + dateAsString: '2022-11-11', + time: '12:12:12', + datetime: d, + year: 22, + datetimeAsString: '2022-11-11 12:12:12', + }); + + const res = await db.select().from(datesTable); + + expect(res[0]?.date).toBeInstanceOf(Date); + expect(res[0]?.datetime).toBeInstanceOf(Date); + expect(typeof res[0]?.dateAsString).toBe('string'); + expect(typeof res[0]?.datetimeAsString).toBe('string'); + + expect(res).toEqual([{ + date: toLocalDate(new Date('2022-11-11')), + dateAsString: '2022-11-11', + time: '12:12:12', + datetime: new Date('2022-11-11'), + year: 2022, + datetimeAsString: '2022-11-11 12:12:12', + }]); + + await db.execute(sql`drop table ${datesTable}`); +}); + +test('SingleStore enum test case #1', async () => { + const tableWithEnums = singlestoreTable('enums_test_case', { + id: serial('id').primaryKey(), + enum1: singlestoreEnum('enum1', ['a', 'b', 'c']).notNull(), + enum2: singlestoreEnum('enum2', ['a', 'b', 'c']).default('a'), + enum3: singlestoreEnum('enum3', ['a', 'b', 'c']).notNull().default('b'), + }); + + await db.execute(sql`drop table if exists ${tableWithEnums}`); + + await db.execute(sql` + create table ${tableWithEnums} ( + \`id\` serial primary key, + \`enum1\` ENUM('a', 'b', 'c') not null, + \`enum2\` ENUM('a', 'b', 'c') default 'a', + \`enum3\` ENUM('a', 'b', 'c') not null default 'b' + ) + `); + + await db.insert(tableWithEnums).values([ + { id: 1, enum1: 'a', enum2: 'b', enum3: 'c' }, + { id: 2, enum1: 'a', enum3: 'c' }, + { id: 3, enum1: 'a' }, + ]); + + const res = await db.select().from(tableWithEnums).orderBy(asc(tableWithEnums.id)); + + await db.execute(sql`drop table ${tableWithEnums}`); + + expect(res).toEqual([ + { id: 1, enum1: 'a', enum2: 'b', enum3: 'c' }, + { id: 2, enum1: 'a', enum2: 'a', enum3: 'c' }, + { id: 3, enum1: 'a', enum2: 'a', enum3: 'b' }, + ]); +}); + +test('left join (flat object fields)', async () => { + await db.insert(citiesTable) + .values([{ id: 1, name: 'Paris' }, { id: 2, name: 'London' }]); + + await db.insert(users2Table).values([{ id: 1, name: 'John', cityId: 1 }, { id: 2, name: 'Jane' }]); + + const res = await db.select({ + userId: users2Table.id, + userName: users2Table.name, + cityId: citiesTable.id, + cityName: citiesTable.name, + }).from(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .orderBy(asc(users2Table.id)); + + expect(res).toEqual([ + { userId: 1, userName: 'John', cityId: 1, cityName: 'Paris' }, + { userId: 2, userName: 'Jane', cityId: null, cityName: null }, + ]); +}); + +test('left join (grouped fields)', async () => { + await db.insert(citiesTable) + .values([{ id: 1, name: 'Paris' }, { id: 2, name: 'London' }]); + + await db.insert(users2Table).values([{ id: 1, name: 'John', cityId: 1 }, { id: 2, name: 'Jane' }]); + + const res = await db.select({ + id: users2Table.id, + user: { + name: users2Table.name, + nameUpper: sql`upper(${users2Table.name})`, + }, + city: { + id: citiesTable.id, + name: citiesTable.name, + nameUpper: sql`upper(${citiesTable.name})`, + }, + }).from(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .orderBy(asc(users2Table.id)); + + expect(res).toEqual([ + { + id: 1, + user: { name: 'John', nameUpper: 'JOHN' }, + city: { id: 1, name: 'Paris', nameUpper: 'PARIS' }, + }, + { + id: 2, + user: { name: 'Jane', nameUpper: 'JANE' }, + city: null, + }, + ]); +}); + +test('left join (all fields)', async () => { + await db.insert(citiesTable) + .values([{ id: 1, name: 'Paris' }, { id: 2, name: 'London' }]); + + await db.insert(users2Table).values([{ id: 1, name: 'John', cityId: 1 }, { id: 2, name: 'Jane' }]); + + const res = await db.select().from(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .orderBy(asc(users2Table.id)); + + expect(res).toEqual([ + { + users2: { + id: 1, + name: 'John', + cityId: 1, + }, + cities: { + id: 1, + name: 'Paris', + }, + }, + { + users2: { + id: 2, + name: 'Jane', + cityId: null, + }, + cities: null, + }, + ]); +}); + +test('join subquery', async () => { + const coursesTable = singlestoreTable('courses', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + categoryId: int('category_id'), + }); + + const courseCategoriesTable = singlestoreTable('course_categories', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${coursesTable}`); + await db.execute(sql`drop table if exists ${courseCategoriesTable}`); + + await db.execute( + sql` + create table ${courseCategoriesTable} ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.execute( + sql` + create table ${coursesTable} ( + \`id\` serial primary key, + \`name\` text not null, + \`category_id\` int + ) + `, + ); + + await db.insert(courseCategoriesTable).values([ + { id: 1, name: 'Category 1' }, + { id: 2, name: 'Category 2' }, + { id: 3, name: 'Category 3' }, + { id: 4, name: 'Category 4' }, + ]); + + await db.insert(coursesTable).values([ + { id: 1, name: 'Development', categoryId: 2 }, + { id: 2, name: 'IT & Software', categoryId: 3 }, + { id: 3, name: 'Marketing', categoryId: 4 }, + { id: 4, name: 'Design', categoryId: 1 }, + ]); + + const sq2 = db + .select({ + categoryId: courseCategoriesTable.id, + category: courseCategoriesTable.name, + total: sql`count(${courseCategoriesTable.id})`, + }) + .from(courseCategoriesTable) + .groupBy(courseCategoriesTable.id, courseCategoriesTable.name) + .orderBy(courseCategoriesTable.id) + .as('sq2'); + + const res = await db + .select({ + courseName: coursesTable.name, + categoryId: sq2.categoryId, + }) + .from(coursesTable) + .leftJoin(sq2, eq(coursesTable.categoryId, sq2.categoryId)) + .orderBy(coursesTable.name); + + await db.execute(sql`drop table ${coursesTable}`); + await db.execute(sql`drop table ${courseCategoriesTable}`); + + expect(res).toEqual([ + { courseName: 'Design', categoryId: 1 }, + { courseName: 'Development', categoryId: 2 }, + { courseName: 'IT & Software', categoryId: 3 }, + { courseName: 'Marketing', categoryId: 4 }, + ]); +}); + +test('with ... select', async () => { + const orders = singlestoreTable('orders', { + id: serial('id').primaryKey(), + region: text('region').notNull(), + product: text('product').notNull(), + amount: int('amount').notNull(), + quantity: int('quantity').notNull(), + }); + + await db.execute(sql`drop table if exists ${orders}`); + await db.execute( + sql` + create table ${orders} ( + \`id\` serial primary key, + \`region\` text not null, + \`product\` text not null, + \`amount\` int not null, + \`quantity\` int not null + ) + `, + ); + + await db.insert(orders).values([ + { region: 'Europe', product: 'A', amount: 10, quantity: 1 }, + { region: 'Europe', product: 'A', amount: 20, quantity: 2 }, + { region: 'Europe', product: 'B', amount: 20, quantity: 2 }, + { region: 'Europe', product: 'B', amount: 30, quantity: 3 }, + { region: 'US', product: 'A', amount: 30, quantity: 3 }, + { region: 'US', product: 'A', amount: 40, quantity: 4 }, + { region: 'US', product: 'B', amount: 40, quantity: 4 }, + { region: 'US', product: 'B', amount: 50, quantity: 5 }, + ]); + + const regionalSales = db + .$with('regional_sales') + .as( + db + .select({ + region: orders.region, + totalSales: sql`sum(${orders.amount})`.as('total_sales'), + }) + .from(orders) + .groupBy(orders.region), + ); + + const topRegions = db + .$with('top_regions') + .as( + db + .select({ + region: regionalSales.region, + }) + .from(regionalSales) + .where( + gt( + regionalSales.totalSales, + db.select({ sales: sql`sum(${regionalSales.totalSales})/10` }).from(regionalSales), + ), + ), + ); + + const result = await db + .with(regionalSales, topRegions) + .select({ + region: orders.region, + product: orders.product, + productUnits: sql`cast(sum(${orders.quantity}) as unsigned)`, + productSales: sql`cast(sum(${orders.amount}) as unsigned)`, + }) + .from(orders) + .where(inArray(orders.region, db.select({ region: topRegions.region }).from(topRegions))) + .groupBy(orders.region, orders.product) + .orderBy(orders.region, orders.product); + + await db.execute(sql`drop table ${orders}`); + + expect(result).toEqual([ + { + region: 'Europe', + product: 'A', + productUnits: 3, + productSales: 30, + }, + { + region: 'Europe', + product: 'B', + productUnits: 5, + productSales: 50, + }, + { + region: 'US', + product: 'A', + productUnits: 7, + productSales: 70, + }, + { + region: 'US', + product: 'B', + productUnits: 9, + productSales: 90, + }, + ]); +}); + +test('select from subquery sql', async () => { + await db.insert(users2Table).values([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]); + + const sq = db + .select({ name: sql`concat(${users2Table.name}, " modified")`.as('name') }) + .from(users2Table) + .orderBy(asc(users2Table.id)) + .as('sq'); + + const res = await db.select({ name: sq.name }).from(sq); + + expect(res).toEqual([{ name: 'John modified' }, { name: 'Jane modified' }]); +}); + +test('select a field without joining its table', () => { + expect(() => db.select({ name: users2Table.name }).from(usersTable).prepare()).toThrowError(); +}); + +test('select all fields from subquery without alias', () => { + const sq = db.$with('sq').as(db.select({ name: sql`upper(${users2Table.name})` }).from(users2Table)); + + expect(() => db.select().from(sq).prepare()).toThrowError(); +}); + +test('select count()', async () => { + await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }]); + + const res = await db.select({ count: sql`count(*)` }).from(usersTable); + + expect(res).toEqual([{ count: 2 }]); +}); + +test('select for ...', () => { + { + const query = db.select().from(users2Table).for('update').toSQL(); + expect(query.sql).toMatch(/ for update$/); + } + { + const query = db.select().from(users2Table).for('share', { skipLocked: true }).toSQL(); + expect(query.sql).toMatch(/ for share skip locked$/); + } + { + const query = db.select().from(users2Table).for('update', { noWait: true }).toSQL(); + expect(query.sql).toMatch(/ for update no wait$/); + } +}); + +test('having', async () => { + await db.insert(citiesTable).values([{ id: 1, name: 'London' }, { id: 2, name: 'Paris' }, { + id: 3, + name: 'New York', + }]); + + await db.insert(users2Table).values([{ id: 1, name: 'John', cityId: 1 }, { id: 2, name: 'Jane', cityId: 1 }, { + id: 3, + name: 'Jack', + cityId: 2, + }]); + + const result = await db + .select({ + id: citiesTable.id, + name: sql`upper(${citiesTable.name})`.as('upper_name'), + usersCount: sql`count(${users2Table.id})`.as('users_count'), + }) + .from(citiesTable) + .leftJoin(users2Table, eq(users2Table.cityId, citiesTable.id)) + .where(({ name }) => sql`length(${name}) >= 3`) + .groupBy(citiesTable.id) + .having(({ usersCount }) => sql`${usersCount} > 0`) + .orderBy(({ name }) => name); + + expect(result).toEqual([ + { + id: 1, + name: 'LONDON', + usersCount: 2, + }, + { + id: 2, + name: 'PARIS', + usersCount: 1, + }, + ]); +}); + +test('view', async () => { + const newYorkers1 = singlestoreView('new_yorkers') + .as((qb) => qb.select().from(users2Table).where(eq(users2Table.cityId, 1))); + + const newYorkers2 = singlestoreView('new_yorkers', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }).as(sql`select * from ${users2Table} where ${eq(users2Table.cityId, 1)}`); + + const newYorkers3 = singlestoreView('new_yorkers', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }).existing(); + + await db.execute(sql`create view new_yorkers as ${getViewConfig(newYorkers1).query}`); + + await db.insert(citiesTable).values([{ name: 'New York' }, { name: 'Paris' }]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + { id: 3, name: 'Jack', cityId: 2 }, + ]); + + { + const result = await db.select().from(newYorkers1).orderBy(asc(newYorkers1.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select().from(newYorkers2).orderBy(asc(newYorkers2.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select().from(newYorkers3).orderBy(asc(newYorkers3.id)); + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 1 }, + ]); + } + + { + const result = await db.select({ name: newYorkers1.name }).from(newYorkers1).orderBy(asc(newYorkers1.id)); + expect(result).toEqual([ + { name: 'John' }, + { name: 'Jane' }, + ]); + } + + await db.execute(sql`drop view ${newYorkers1}`); +}); + +test('select from raw sql', async () => { + const result = await db.select({ + id: sql`id`, + name: sql`name`, + }).from(sql`(select 1 as id, 'John' as name) as users`); + + Expect>; + + expect(result).toEqual([ + { id: 1, name: 'John' }, + ]); +}); + +test('select from raw sql with joins', async () => { + const result = await db + .select({ + id: sql`users.id`, + name: sql`users.name`, + userCity: sql`users.city`, + cityName: sql`cities.name`, + }) + .from(sql`(select 1 as id, 'John' as name, 'New York' as city) as users`) + .leftJoin(sql`(select 1 as id, 'Paris' as name) as cities`, sql`cities.id = users.id`); + + Expect>; + + expect(result).toEqual([ + { id: 1, name: 'John', userCity: 'New York', cityName: 'Paris' }, + ]); +}); + +test('join on aliased sql from select', async () => { + const result = await db + .select({ + userId: sql`users.id`.as('userId'), + name: sql`users.name`, + userCity: sql`users.city`, + cityId: sql`cities.id`.as('cityId'), + cityName: sql`cities.name`, + }) + .from(sql`(select 1 as id, 'John' as name, 'New York' as city) as users`) + .leftJoin(sql`(select 1 as id, 'Paris' as name) as cities`, (cols) => eq(cols.cityId, cols.userId)); + + Expect>; + + expect(result).toEqual([ + { userId: 1, name: 'John', userCity: 'New York', cityId: 1, cityName: 'Paris' }, + ]); +}); + +test('join on aliased sql from with clause', async () => { + const users = db.$with('users').as( + db.select({ + id: sql`id`.as('userId'), + name: sql`name`.as('userName'), + city: sql`city`.as('city'), + }).from( + sql`(select 1 as id, 'John' as name, 'New York' as city) as users`, + ), + ); + + const cities = db.$with('cities').as( + db.select({ + id: sql`id`.as('cityId'), + name: sql`name`.as('cityName'), + }).from( + sql`(select 1 as id, 'Paris' as name) as cities`, + ), + ); + + const result = await db + .with(users, cities) + .select({ + userId: users.id, + name: users.name, + userCity: users.city, + cityId: cities.id, + cityName: cities.name, + }) + .from(users) + .leftJoin(cities, (cols) => eq(cols.cityId, cols.userId)); + + Expect>; + + expect(result).toEqual([ + { userId: 1, name: 'John', userCity: 'New York', cityId: 1, cityName: 'Paris' }, + ]); +}); + +test('prefixed table', async () => { + const singlestoreTable = singlestoreTableCreator((name) => `myprefix_${name}`); + + const users = singlestoreTable('test_prefixed_table_with_unique_name', { + id: int('id').primaryKey(), + name: text('name').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table myprefix_test_prefixed_table_with_unique_name (id int not null primary key, name text not null)`, + ); + + await db.insert(users).values({ id: 1, name: 'John' }); + + const result = await db.select().from(users); + + expect(result).toEqual([{ id: 1, name: 'John' }]); + + await db.execute(sql`drop table ${users}`); +}); + +test('orderBy with aliased column', () => { + const query = db.select({ + test: sql`something`.as('test'), + }).from(users2Table).orderBy((fields) => fields.test).toSQL(); + + expect(query.sql).toBe(`select something as \`test\` from \`${getTableName(users2Table)}\` order by \`test\``); +}); + +test('timestamp timezone', async () => { + const date = new Date(Date.parse('2020-01-01T12:34:56+07:00')); + + await db.insert(usersTable).values({ id: 1, name: 'With default times' }); + await db.insert(usersTable).values({ + id: 2, + name: 'Without default times', + createdAt: date, + }); + const users = await db.select().from(usersTable).orderBy(asc(usersTable.id)); + + // check that the timestamps are set correctly for default times + expect(Math.abs(users[0]!.createdAt.getTime() - Date.now())).toBeLessThan(2000); + + // check that the timestamps are set correctly for non default times + expect(Math.abs(users[1]!.createdAt.getTime() - date.getTime())).toBeLessThan(2000); +}); + +test('transaction', async () => { + const users = singlestoreTable('users_transactions', { + id: serial('id').primaryKey(), + balance: int('balance').notNull(), + }); + const products = singlestoreTable('products_transactions', { + id: serial('id').primaryKey(), + price: int('price').notNull(), + stock: int('stock').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`drop table if exists ${products}`); + + await db.execute(sql`create table ${users} (id serial not null primary key, balance int not null)`); + await db.execute( + sql`create table ${products} (id serial not null primary key, price int not null, stock int not null)`, + ); + + const [{ insertId: userId }] = await db.insert(users).values({ id: 1, balance: 100 }); + const user = await db.select().from(users).where(eq(users.id, userId)).then((rows) => rows[0]!); + const [{ insertId: productId }] = await db.insert(products).values({ id: 1, price: 10, stock: 10 }); + const product = await db.select().from(products).where(eq(products.id, productId)).then((rows) => rows[0]!); + + await db.transaction(async (tx) => { + await tx.update(users).set({ balance: user.balance - product.price }).where(eq(users.id, user.id)); + await tx.update(products).set({ stock: product.stock - 1 }).where(eq(products.id, product.id)); + }); + + const result = await db.select().from(users); + + await db.execute(sql`drop table ${users}`); + await db.execute(sql`drop table ${products}`); + + expect(result).toEqual([{ id: 1, balance: 90 }]); +}); + +test('transaction rollback', async () => { + const users = singlestoreTable('users_transactions_rollback', { + id: serial('id').primaryKey(), + balance: int('balance').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, balance int not null)`, + ); + + await expect((async () => { + await db.transaction(async (tx) => { + await tx.insert(users).values({ balance: 100 }); + tx.rollback(); + }); + })()).rejects.toThrowError(TransactionRollbackError); + + const result = await db.select().from(users); + + await db.execute(sql`drop table ${users}`); + + expect(result).toEqual([]); +}); + +test('join subquery with join', async () => { + const internalStaff = singlestoreTable('internal_staff', { + userId: int('user_id').notNull(), + }); + + const customUser = singlestoreTable('custom_user', { + id: int('id').notNull(), + }); + + const ticket = singlestoreTable('ticket', { + staffId: int('staff_id').notNull(), + }); + + await db.execute(sql`drop table if exists ${internalStaff}`); + await db.execute(sql`drop table if exists ${customUser}`); + await db.execute(sql`drop table if exists ${ticket}`); + + await db.execute(sql`create table ${internalStaff} (user_id integer not null)`); + await db.execute(sql`create table ${customUser} (id integer not null)`); + await db.execute(sql`create table ${ticket} (staff_id integer not null)`); + + await db.insert(internalStaff).values({ userId: 1 }); + await db.insert(customUser).values({ id: 1 }); + await db.insert(ticket).values({ staffId: 1 }); + + const subq = db + .select() + .from(internalStaff) + .leftJoin(customUser, eq(internalStaff.userId, customUser.id)) + .as('internal_staff'); + + const mainQuery = await db + .select() + .from(ticket) + .leftJoin(subq, eq(subq.internal_staff.userId, ticket.staffId)); + + await db.execute(sql`drop table ${internalStaff}`); + await db.execute(sql`drop table ${customUser}`); + await db.execute(sql`drop table ${ticket}`); + + expect(mainQuery).toEqual([{ + ticket: { staffId: 1 }, + internal_staff: { + internal_staff: { userId: 1 }, + custom_user: { id: 1 }, + }, + }]); +}); + +test('subquery with view', async () => { + const users = singlestoreTable('users_subquery_view', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }); + + const newYorkers = singlestoreView('new_yorkers').as((qb) => qb.select().from(users).where(eq(users.cityId, 1))); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`drop view if exists ${newYorkers}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, name text not null, city_id integer not null)`, + ); + await db.execute(sql`create view ${newYorkers} as select * from ${users} where city_id = 1`); + + await db.insert(users).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 1 }, + { id: 4, name: 'Jill', cityId: 2 }, + ]); + + const sq = db.$with('sq').as(db.select().from(newYorkers)); + const result = await db.with(sq).select().from(sq).orderBy(asc(sq.id)); + + await db.execute(sql`drop view ${newYorkers}`); + await db.execute(sql`drop table ${users}`); + + expect(result).toEqual([ + { id: 1, name: 'John', cityId: 1 }, + { id: 3, name: 'Jack', cityId: 1 }, + ]); +}); + +test('join view as subquery', async () => { + const users = singlestoreTable('users_join_view', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: int('city_id').notNull(), + }); + + const newYorkers = singlestoreView('new_yorkers').as((qb) => qb.select().from(users).where(eq(users.cityId, 1))); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`drop view if exists ${newYorkers}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, name text not null, city_id integer not null)`, + ); + await db.execute(sql`create view ${newYorkers} as select * from ${users} where city_id = 1`); + + await db.insert(users).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 1 }, + { id: 4, name: 'Jill', cityId: 2 }, + ]); + + const sq = db.select().from(newYorkers).as('new_yorkers_sq'); + + const result = await db.select().from(users).leftJoin(sq, eq(users.id, sq.id)).orderBy(asc(users.id)); + + expect(result).toEqual([ + { + users_join_view: { id: 1, name: 'John', cityId: 1 }, + new_yorkers_sq: { id: 1, name: 'John', cityId: 1 }, + }, + { + users_join_view: { id: 2, name: 'Jane', cityId: 2 }, + new_yorkers_sq: null, + }, + { + users_join_view: { id: 3, name: 'Jack', cityId: 1 }, + new_yorkers_sq: { id: 3, name: 'Jack', cityId: 1 }, + }, + { + users_join_view: { id: 4, name: 'Jill', cityId: 2 }, + new_yorkers_sq: null, + }, + ]); + + await db.execute(sql`drop view ${newYorkers}`); + await db.execute(sql`drop table ${users}`); +}); + +test('select iterator', async () => { + const users = singlestoreTable('users_iterator', { + id: serial('id').primaryKey(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial not null primary key)`); + + await db.insert(users).values([{ id: 1 }, { id: 2 }, { id: 3 }]); + + const iter = db.select().from(users) + .orderBy(asc(users.id)) + .iterator(); + + const result: typeof users.$inferSelect[] = []; + + for await (const row of iter) { + result.push(row); + } + + expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); +}); + +test('select iterator w/ prepared statement', async () => { + const users = singlestoreTable('users_iterator', { + id: serial('id').primaryKey(), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute(sql`create table ${users} (id serial not null primary key)`); + + await db.insert(users).values([{ id: 1 }, { id: 2 }, { id: 3 }]); + + const prepared = db.select().from(users) + .orderBy(asc(users.id)) + .prepare(); + const iter = prepared.iterator(); + const result: typeof users.$inferSelect[] = []; + + for await (const row of iter) { + result.push(row); + } + + expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); +}); + +test('insert undefined', async () => { + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name'), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, name text)`, + ); + + await expect((async () => { + await db.insert(users).values({ name: undefined }); + })()).resolves.not.toThrowError(); + + await db.execute(sql`drop table ${users}`); +}); + +test('update undefined', async () => { + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name'), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute( + sql`create table ${users} (id serial not null primary key, name text)`, + ); + + await expect((async () => { + await db.update(users).set({ name: undefined }); + })()).rejects.toThrowError(); + + await expect((async () => { + await db.update(users).set({ id: 1, name: undefined }); + })()).resolves.not.toThrowError(); + + await db.execute(sql`drop table ${users}`); +}); diff --git a/integration-tests/tests/singlestore/singlestore-proxy.test.ts b/integration-tests/tests/singlestore/singlestore-proxy.test.ts new file mode 100644 index 000000000..51dc48a4a --- /dev/null +++ b/integration-tests/tests/singlestore/singlestore-proxy.test.ts @@ -0,0 +1,140 @@ +import retry from 'async-retry'; +import type { SingleStoreRemoteDatabase } from 'drizzle-orm/singlestore-proxy'; +import { drizzle as proxyDrizzle } from 'drizzle-orm/singlestore-proxy'; +import * as mysql2 from 'mysql2/promise'; +import { afterAll, beforeAll, beforeEach } from 'vitest'; +import { skipTests } from '~/common'; +import { createDockerDB, tests } from './singlestore-common'; + +const ENABLE_LOGGING = false; + +// eslint-disable-next-line drizzle-internal/require-entity-kind +class ServerSimulator { + constructor(private db: mysql2.Connection) {} + + async query(sql: string, params: any[], method: 'all' | 'execute') { + if (method === 'all') { + try { + const result = await this.db.query({ + sql, + values: params, + rowsAsArray: true, + typeCast: function(field: any, next: any) { + if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') { + return field.string(); + } + return next(); + }, + }); + + return { data: result[0] as any }; + } catch (e: any) { + return { error: e }; + } + } else if (method === 'execute') { + try { + const result = await this.db.query({ + sql, + values: params, + typeCast: function(field: any, next: any) { + if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') { + return field.string(); + } + return next(); + }, + }); + + return { data: result as any }; + } catch (e: any) { + return { error: e }; + } + } else { + return { error: 'Unknown method value' }; + } + } + + async migrations(queries: string[]) { + await this.db.query('START TRANSACTION'); + try { + for (const query of queries) { + await this.db.query(query); + } + await this.db.query('COMMIT'); + } catch (e) { + await this.db.query('ROLLBACK'); + throw e; + } + + return {}; + } +} + +let db: SingleStoreRemoteDatabase; +let client: mysql2.Connection; +let serverSimulator: ServerSimulator; + +beforeAll(async () => { + let connectionString; + if (process.env['SINGLESTORE_CONNECTION_STRING']) { + connectionString = process.env['SINGLESTORE_CONNECTION_STRING']; + } else { + const { connectionString: conStr } = await createDockerDB(); + connectionString = conStr; + } + client = await retry(async () => { + client = await mysql2.createConnection(connectionString); + await client.connect(); + return client; + }, { + retries: 20, + factor: 1, + minTimeout: 250, + maxTimeout: 250, + randomize: false, + onRetry() { + client?.end(); + }, + }); + + await client.query(`CREATE DATABASE IF NOT EXISTS drizzle;`); + await client.changeUser({ database: 'drizzle' }); + + serverSimulator = new ServerSimulator(client); + db = proxyDrizzle(async (sql, params, method) => { + try { + const response = await serverSimulator.query(sql, params, method); + + if (response.error !== undefined) { + throw response.error; + } + + return { rows: response.data }; + } catch (e: any) { + console.error('Error from singlestore proxy server:', e.message); + throw e; + } + }, { logger: ENABLE_LOGGING }); +}); + +afterAll(async () => { + await client?.end(); +}); + +beforeEach((ctx) => { + ctx.singlestore = { + db, + }; +}); + +skipTests([ + 'select iterator w/ prepared statement', + 'select iterator', + 'nested transaction rollback', + 'nested transaction', + 'transaction rollback', + 'transaction', + 'transaction with options (set isolationLevel)', + 'migrator', +]); + +tests(); diff --git a/integration-tests/tests/singlestore/singlestore.test.ts b/integration-tests/tests/singlestore/singlestore.test.ts new file mode 100644 index 000000000..bfb1ee5b7 --- /dev/null +++ b/integration-tests/tests/singlestore/singlestore.test.ts @@ -0,0 +1,51 @@ +import retry from 'async-retry'; +import { drizzle } from 'drizzle-orm/singlestore'; +import type { SingleStoreDriverDatabase } from 'drizzle-orm/singlestore'; +import * as mysql2 from 'mysql2/promise'; +import { afterAll, beforeAll, beforeEach } from 'vitest'; +import { createDockerDB, tests } from './singlestore-common'; + +const ENABLE_LOGGING = false; + +let db: SingleStoreDriverDatabase; +let client: mysql2.Connection; + +beforeAll(async () => { + let connectionString; + if (process.env['SINGLESTORE_CONNECTION_STRING']) { + connectionString = process.env['SINGLESTORE_CONNECTION_STRING']; + } else { + const { connectionString: conStr } = await createDockerDB(); + connectionString = conStr; + } + client = await retry(async () => { + client = await mysql2.createConnection(connectionString); + await client.connect(); + return client; + }, { + retries: 20, + factor: 1, + minTimeout: 250, + maxTimeout: 250, + randomize: false, + onRetry() { + client?.end(); + }, + }); + + await client.query(`CREATE DATABASE IF NOT EXISTS drizzle;`); + await client.changeUser({ database: 'drizzle' }); + db = drizzle(client, { logger: ENABLE_LOGGING }); +}); + +afterAll(async () => { + await client?.end(); +}); + +beforeEach((ctx) => { + ctx.singlestore = { + db, + }; +}); + +tests(); diff --git a/integration-tests/vitest.config.ts b/integration-tests/vitest.config.ts index 5187d2cfc..cd4ddf621 100644 --- a/integration-tests/vitest.config.ts +++ b/integration-tests/vitest.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ 'tests/imports/**/*', 'tests/extensions/vectors/**/*', 'tests/version.test.ts', + 'tests/singlestore/**/*.test.ts', ], exclude: [ ...(process.env.SKIP_EXTERNAL_DB_TESTS @@ -25,7 +26,6 @@ export default defineConfig({ 'tests/sqlite/libsql.test.ts', 'tests/mysql/tidb-serverless.test.ts', 'tests/sqlite/libsql-batch.test.ts', - 'tests/pg/neon-http.test.ts', 'tests/pg/neon-http-batch.test.ts', ]