Files
2026-07-13 12:10:05 +08:00

4789 lines
121 KiB
TypeScript

/* 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,
binary,
boolean,
char,
date,
datetime,
decimal,
double,
except,
float,
getTableConfig,
index,
int,
intersect,
json,
mediumint,
primaryKey,
real,
serial,
/* singlestoreView, */
SingleStoreDialect,
singlestoreEnum,
singlestoreSchema,
singlestoreTable,
singlestoreTableCreator,
smallint,
text,
time,
timestamp,
tinyint,
union,
unionAll,
unique,
uniqueIndex,
uniqueKeyName,
varbinary,
varchar,
vector,
year,
} from 'drizzle-orm/singlestore-core';
import { dotProduct, euclideanDistance } from 'drizzle-orm/singlestore-core/expressions';
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<any, any>;
declare module 'vitest' {
interface TestContext {
singlestore: {
db: TestSingleStoreDB;
};
}
}
const ENABLE_LOGGING = false;
const allTypesTable = singlestoreTable('all_types', {
serial: serial('scol'),
bigint53: bigint('bigint53', {
mode: 'number',
}),
bigint64: bigint('bigint64', {
mode: 'bigint',
}),
binary: binary('binary'),
boolean: boolean('boolean'),
char: char('char'),
date: date('date', {
mode: 'date',
}),
dateStr: date('date_str', {
mode: 'string',
}),
datetime: datetime('datetime', {
mode: 'date',
}),
datetimeStr: datetime('datetime_str', {
mode: 'string',
}),
decimal: decimal('decimal'),
decimalNum: decimal('decimal_num', {
scale: 30,
mode: 'number',
}),
decimalBig: decimal('decimal_big', {
scale: 30,
mode: 'bigint',
}),
double: double('double'),
float: float('float'),
int: int('int'),
json: json('json'),
medInt: mediumint('med_int'),
smallInt: smallint('small_int'),
real: real('real'),
text: text('text'),
time: time('time'),
timestamp: timestamp('timestamp', {
mode: 'date',
}),
timestampStr: timestamp('timestamp_str', {
mode: 'string',
}),
tinyInt: tinyint('tiny_int'),
varbin: varbinary('varbin', {
length: 16,
}),
varchar: varchar('varchar', {
length: 255,
}),
year: year('year'),
enum: singlestoreEnum('enum', ['enV1', 'enV2']),
});
const usersTable = singlestoreTable('userstest', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
verified: boolean('verified').notNull().default(false),
jsonb: json('jsonb').$type<string[]>(),
createdAt: timestamp('created_at').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' }).$onUpdateFn(
() => new Date(),
),
alwaysNull: text('always_null')
.$type<string | null>()
.$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'),
datetime: datetime('datetime'),
datetimeAsString: datetime('datetime_as_string', { mode: 'string' }),
timestamp: timestamp('timestamp'),
timestampAsString: timestamp('timestamp_as_string', { 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'),
});
const vectorSearchTable = singlestoreTable('vector_search', {
id: serial('id').notNull(),
text: text('text').notNull(),
embedding: vector('embedding', { dimensions: 10 }),
});
// 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<string[]>(),
createdAt: timestamp('created_at').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,
};
}
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 table if exists ${allTypesTable}`);
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
)
`,
);
});
async function setupReturningFunctionsTest(
db: SingleStoreDatabase<any, any>,
) {
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 },
]);
}
async function setupVectorSearchTest(db: TestSingleStoreDB) {
await db.execute(sql`drop table if exists \`vector_search\``);
await db.execute(
sql`
create table \`vector_search\` (
\`id\` integer primary key auto_increment not null,
\`text\` text not null,
\`embedding\` vector(10) not null
)
`,
);
await db.insert(vectorSearchTable).values([
{
id: 1,
text: 'I like dogs',
embedding: [
0.6119,
0.1395,
0.2921,
0.3664,
0.4561,
0.7852,
0.1997,
0.5142,
0.5924,
0.0465,
],
},
{
id: 2,
text: 'I like cats',
embedding: [
0.6075,
0.1705,
0.0651,
0.9489,
0.9656,
0.8084,
0.3046,
0.0977,
0.6842,
0.4402,
],
},
]);
}
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<string>`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<string>`upper(${users2Table.name})`,
},
city: {
id: citiesTable.id,
name: citiesTable.name,
nameUpper: sql<string>`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('select from a many subquery', async (ctx) => {
const { db } = ctx.singlestore;
await db
.insert(citiesTable)
.values([{ name: 'Paris' }, { name: 'London' }]);
await db.insert(users2Table).values([
{ name: 'John', cityId: 1 },
{ name: 'Jane', cityId: 2 },
{ name: 'Jack', cityId: 2 },
]);
const res = await db
.select({
population: db
.select({ count: count().as('count') })
.from(users2Table)
.where(eq(users2Table.cityId, citiesTable.id))
.as('population'),
name: citiesTable.name,
})
.from(citiesTable);
expectTypeOf(res).toEqualTypeOf<
{
population: number;
name: string;
}[]
>();
expect(res).toStrictEqual(
expect.arrayContaining([
{
population: 1,
name: 'Paris',
},
{
population: 2,
name: 'London',
},
]),
);
});
test('select from a one subquery', async (ctx) => {
const { db } = ctx.singlestore;
await db
.insert(citiesTable)
.values([{ name: 'Paris' }, { name: 'London' }]);
await db.insert(users2Table).values([
{ name: 'John', cityId: 1 },
{ name: 'Jane', cityId: 2 },
{ name: 'Jack', cityId: 2 },
]);
const res = await db
.select({
cityName: db
.select({ name: citiesTable.name })
.from(citiesTable)
.where(eq(users2Table.cityId, citiesTable.id))
.as('cityName'),
name: users2Table.name,
})
.from(users2Table);
expectTypeOf(res).toEqualTypeOf<
{
cityName: string;
name: string;
}[]
>();
expect(res).toStrictEqual(
expect.arrayContaining([
{
cityName: 'Paris',
name: 'John',
},
{
cityName: 'London',
name: 'Jane',
},
{
cityName: 'London',
name: 'Jack',
},
]),
);
});
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<number>`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<number>`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<number>`cast(sum(${orders.quantity}) as unsigned)`,
productSales: sql<number>`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<string>`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<string>`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 nowait$/);
}
});
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<string>`upper(${citiesTable.name})`.as('upper_name'),
usersCount: sql<number>`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,
},
]);
});
// TODO: Unskip when views are supported
/* test.skip('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<number>`id`,
name: sql<string>`name`,
})
.from(sql`(select 1 as id, 'John' as name) as users`);
Expect<Equal<{ id: number; name: string }[], typeof result>>;
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<number>`users.id`,
name: sql<string>`users.name`,
userCity: sql<string>`users.city`,
cityName: sql<string>`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<
Equal<
{ id: number; name: string; userCity: string; cityName: string }[],
typeof result
>
>;
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<number>`users.id`.as('userId'),
name: sql<string>`users.name`,
userCity: sql<string>`users.city`,
cityId: sql<number>`cities.id`.as('cityId'),
cityName: sql<string>`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<number>`id`.as('userId'),
name: sql<string>`name`.as('userName'),
city: sql<string>`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<number>`id`.as('cityId'),
name: sql<string>`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}`);
});
// TODO: Unskip when views are supported
/* test.skip('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}`);
}); */
// TODO: Unskip when views are supported
/* test.skip('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', { mode: 'date' }),
datetime: datetime('datetime'),
});
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 = 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).toContainEqual({ id: 2, name: 'London' });
expect(result).toContainEqual({ 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('define constraints as array', async (ctx) => {
const { db } = ctx.singlestore;
const table = singlestoreTable(
'name',
{
id: int(),
},
(t) => [
index('name').on(t.id),
primaryKey({ columns: [t.id], name: 'custom' }),
],
);
const { indexes, primaryKeys } = getTableConfig(table);
expect(indexes.length).toBe(1);
expect(primaryKeys.length).toBe(1);
});
test('define constraints as array inside third param', async (ctx) => {
const { db } = ctx.singlestore;
const table = singlestoreTable(
'name',
{
id: int(),
},
(t) => [
[
index('name').on(t.id),
primaryKey({ columns: [t.id], name: 'custom' }),
],
],
);
const { indexes, primaryKeys } = getTableConfig(table);
expect(indexes.length).toBe(1);
expect(primaryKeys.length).toBe(1);
});
test.skip('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('simple vector search', async (ctx) => {
const { db } = ctx.singlestore;
const table = vectorSearchTable;
const embedding = [
0.42,
0.93,
0.88,
0.57,
0.32,
0.64,
0.76,
0.52,
0.19,
0.81,
]; // ChatGPT's 10 dimension embedding for "dogs are cool" not sure how accurate but it works
await setupVectorSearchTest(db);
const withRankEuclidean = db
.select({
id: table.id,
text: table.text,
rank: sql`row_number() over (order by ${euclideanDistance(table.embedding, embedding)})`.as(
'rank',
),
})
.from(table)
.as('with_rank');
const withRankDotProduct = db
.select({
id: table.id,
text: table.text,
rank: sql`row_number() over (order by ${dotProduct(table.embedding, embedding)})`.as(
'rank',
),
})
.from(table)
.as('with_rank');
const result1 = await db
.select({ id: withRankEuclidean.id, text: withRankEuclidean.text })
.from(withRankEuclidean)
.where(eq(withRankEuclidean.rank, 1));
const result2 = await db
.select({ id: withRankDotProduct.id, text: withRankDotProduct.text })
.from(withRankDotProduct)
.where(eq(withRankDotProduct.rank, 1));
expect(result1.length).toEqual(1);
expect(result1[0]).toEqual({ id: 1, text: 'I like dogs' });
expect(result2.length).toEqual(1);
expect(result2[0]).toEqual({ id: 1, text: 'I like dogs' });
});
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 = 2000;
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<string>`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 \`mySchema\`.\`userstest\`.\`id\`, \`mySchema\`.\`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' },
]);
});
// TODO: Unkip this test when views are supported
/* test.skip('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);
});
test('sql operator as cte', async (ctx) => {
const { db } = ctx.singlestore;
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 not null primary key, name text not null)`,
);
await db.insert(users).values([{ name: 'John' }, { name: 'Jane' }]);
const sq1 = db
.$with('sq', {
userId: users.id,
data: {
name: users.name,
},
})
.as(sql`select * from ${users} where ${users.name} = 'John'`);
const result1 = await db.with(sq1).select().from(sq1);
const sq2 = db
.$with('sq', {
userId: users.id,
data: {
name: users.name,
},
})
.as(() => sql`select * from ${users} where ${users.name} = 'Jane'`);
const result2 = await db.with(sq2).select().from(sq1);
expect(result1).toEqual([{ userId: 1, data: { name: 'John' } }]);
expect(result2).toEqual([{ userId: 2, data: { name: 'Jane' } }]);
});
test('cross join', async (ctx) => {
const { db } = ctx.singlestore;
await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }]);
await db
.insert(citiesTable)
.values([{ name: 'Seattle' }, { name: 'New York City' }]);
const result = await db
.select({
user: usersTable.name,
city: citiesTable.name,
})
.from(usersTable)
.crossJoin(citiesTable)
.orderBy(usersTable.name, citiesTable.name);
expect(result).toStrictEqual([
{ city: 'New York City', user: 'Jane' },
{ city: 'Seattle', user: 'Jane' },
{ city: 'New York City', user: 'John' },
{ city: 'Seattle', user: 'John' },
]);
});
test('left join (lateral)', async (ctx) => {
const { db } = ctx.singlestore;
await db.insert(citiesTable).values([
{ id: 1, name: 'Paris' },
{ id: 2, name: 'London' },
]);
await db
.insert(users2Table)
.values([{ name: 'John', cityId: 1 }, { name: 'Jane' }]);
const sq = db
.select({
userId: users2Table.id,
userName: users2Table.name,
cityId: users2Table.cityId,
})
.from(users2Table)
.where(eq(users2Table.cityId, citiesTable.id))
.as('sq');
const res = await db
.select({
cityId: citiesTable.id,
cityName: citiesTable.name,
userId: sq.userId,
userName: sq.userName,
})
.from(citiesTable)
.leftJoinLateral(sq, sql`true`)
.orderBy(citiesTable.id);
expect(res).toStrictEqual([
{ cityId: 1, cityName: 'Paris', userId: 1, userName: 'John' },
{ cityId: 2, cityName: 'London', userId: null, userName: null },
]);
});
test('inner join (lateral)', async (ctx) => {
const { db } = ctx.singlestore;
await db.insert(citiesTable).values([
{ id: 1, name: 'Paris' },
{ id: 2, name: 'London' },
]);
await db
.insert(users2Table)
.values([{ name: 'John', cityId: 1 }, { name: 'Jane' }]);
const sq = db
.select({
userId: users2Table.id,
userName: users2Table.name,
cityId: users2Table.cityId,
})
.from(users2Table)
.where(eq(users2Table.cityId, citiesTable.id))
.as('sq');
const res = await db
.select({
cityId: citiesTable.id,
cityName: citiesTable.name,
userId: sq.userId,
userName: sq.userName,
})
.from(citiesTable)
.innerJoinLateral(sq, sql`true`);
expect(res).toStrictEqual([
{ cityId: 1, cityName: 'Paris', userId: 1, userName: 'John' },
]);
});
test('cross join (lateral)', async (ctx) => {
const { db } = ctx.singlestore;
await db.insert(citiesTable).values([
{ id: 1, name: 'Paris' },
{ id: 2, name: 'London' },
]);
await db.insert(users2Table).values([
{ name: 'John', cityId: 1 },
{ name: 'Jane', cityId: 2 },
{
name: 'Patrick',
cityId: 2,
},
]);
const sq = db
.select({
userId: users2Table.id,
userName: users2Table.name,
cityId: users2Table.cityId,
})
.from(users2Table)
.where(eq(users2Table.cityId, citiesTable.id))
.as('sq');
const res = await db
.select({
cityId: citiesTable.id,
cityName: citiesTable.name,
userId: sq.userId,
userName: sq.userName,
})
.from(citiesTable)
.crossJoinLateral(sq)
.orderBy(sq.userId, citiesTable.id);
expect(res).toStrictEqual([
{
cityId: 1,
cityName: 'Paris',
userId: 1,
userName: 'John',
},
{
cityId: 2,
cityName: 'London',
userId: 2,
userName: 'Jane',
},
{
cityId: 2,
cityName: 'London',
userId: 3,
userName: 'Patrick',
},
]);
});
test('test $onUpdateFn and $onUpdate works with sql value', async (ctx) => {
const { db } = ctx.singlestore;
const users = singlestoreTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
updatedAt: timestamp('updated_at')
.notNull()
.$onUpdate(() => sql`current_timestamp`),
});
await db.execute(sql`drop table if exists ${users}`);
await db.execute(
sql`
create table ${users} (
\`id\` serial primary key,
\`name\` text not null,
\`updated_at\` timestamp not null
)
`,
);
await db.insert(users).values({
name: 'John',
});
const insertResp = await db
.select({ updatedAt: users.updatedAt })
.from(users);
await new Promise((resolve) => setTimeout(resolve, 1000));
const now = Date.now();
await new Promise((resolve) => setTimeout(resolve, 1000));
await db.update(users).set({
name: 'John',
});
const updateResp = await db
.select({ updatedAt: users.updatedAt })
.from(users);
expect(insertResp[0]?.updatedAt.getTime() ?? 0).lessThan(now);
expect(updateResp[0]?.updatedAt.getTime() ?? 0).greaterThan(now);
});
test('all types', async (ctx) => {
const { db } = ctx.singlestore;
await db.execute(sql`
CREATE TABLE \`all_types\` (
\`scol\` serial,
\`bigint53\` bigint,
\`bigint64\` bigint,
\`binary\` binary,
\`boolean\` boolean,
\`char\` char,
\`date\` date,
\`date_str\` date,
\`datetime\` datetime,
\`datetime_str\` datetime,
\`decimal\` decimal,
\`decimal_num\` decimal(30),
\`decimal_big\` decimal(30),
\`double\` double,
\`float\` float,
\`int\` int,
\`json\` json,
\`med_int\` mediumint,
\`small_int\` smallint,
\`real\` real,
\`text\` text,
\`time\` time,
\`timestamp\` timestamp,
\`timestamp_str\` timestamp,
\`tiny_int\` tinyint,
\`varbin\` varbinary(16),
\`varchar\` varchar(255),
\`year\` year,
\`enum\` enum('enV1','enV2'),
shard key(\`scol\`)
);
`);
await db.insert(allTypesTable).values({
serial: 1,
bigint53: 9007199254740991,
bigint64: 5044565289845416380n,
binary: '1',
boolean: true,
char: 'c',
date: new Date(1741743161623),
dateStr: new Date(1741743161623)
.toISOString()
.slice(0, 19)
.replace('T', ' '),
datetime: new Date(1741743161623),
datetimeStr: new Date(1741743161623)
.toISOString()
.slice(0, 19)
.replace('T', ' '),
decimal: '47521',
decimalNum: 9007199254740991,
decimalBig: 5044565289845416380n,
double: 15.35325689124218,
enum: 'enV1',
float: 1.048596,
real: 1.048596,
text: 'C4-',
int: 621,
json: {
str: 'strval',
arr: ['str', 10],
},
medInt: 560,
smallInt: 14,
time: '04:13:22',
timestamp: new Date(1741743161623),
timestampStr: new Date(1741743161623)
.toISOString()
.slice(0, 19)
.replace('T', ' '),
tinyInt: 7,
varbin: '1010110101001101',
varchar: 'VCHAR',
year: 2025,
});
const rawRes = await db.select().from(allTypesTable);
type ExpectedType = {
serial: number;
bigint53: number | null;
bigint64: bigint | null;
binary: string | null;
boolean: boolean | null;
char: string | null;
date: Date | null;
dateStr: string | null;
datetime: Date | null;
datetimeStr: string | null;
decimal: string | null;
decimalNum: number | null;
decimalBig: bigint | null;
double: number | null;
float: number | null;
int: number | null;
json: unknown;
medInt: number | null;
smallInt: number | null;
real: number | null;
text: string | null;
time: string | null;
timestamp: Date | null;
timestampStr: string | null;
tinyInt: number | null;
varbin: string | null;
varchar: string | null;
year: number | null;
enum: 'enV1' | 'enV2' | null;
}[];
const expectedRes: ExpectedType = [
{
serial: 1,
bigint53: 9007199254740991,
bigint64: 5044565289845416380n,
binary: '1',
boolean: true,
char: 'c',
date: new Date('2025-03-12T00:00:00.000Z'),
dateStr: '2025-03-12',
datetime: new Date('2025-03-12T01:32:41.000Z'),
datetimeStr: '2025-03-12 01:32:41',
decimal: '47521',
decimalNum: 9007199254740991,
decimalBig: 5044565289845416380n,
double: 15.35325689124218,
float: 1.0486,
int: 621,
json: { arr: ['str', 10], str: 'strval' },
medInt: 560,
smallInt: 14,
real: 1.048596,
text: 'C4-',
time: '04:13:22',
timestamp: new Date('2025-03-12T01:32:41.000Z'),
timestampStr: '2025-03-12 01:32:41',
tinyInt: 7,
varbin: '1010110101001101',
varchar: 'VCHAR',
year: 2025,
enum: 'enV1',
},
];
expectTypeOf(rawRes).toEqualTypeOf<ExpectedType>();
expect(rawRes).toStrictEqual(expectedRes);
});
});
test('sql.identifier escape', async (ctx) => {
const { db } = ctx.singlestore;
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 not null primary key, name text not null)`,
);
await db.insert(users).values([{ name: 'John' }, { name: 'Jane' }]);
const singlestoreDialect = new SingleStoreDialect();
const userInput = 'id` ASC, CAST((SELECT name FROM users LIMIT 1) AS int)--';
const query = sql`SELECT * FROM ${sql.identifier('users')} ORDER BY ${sql.identifier(userInput)} ASC`;
const str = singlestoreDialect.sqlToQuery(query);
expect(str.sql).toBe(
'SELECT * FROM `users` ORDER BY `id`` ASC, CAST((SELECT name FROM users LIMIT 1) AS int)--` ASC',
);
});
}