chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { type BetterSQLite3Database, drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
||||
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { anotherUsersMigratorTable, tests, usersMigratorTable } from './sqlite-common';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
let db: BetterSQLite3Database;
|
||||
let client: Database.Database;
|
||||
|
||||
beforeAll(async () => {
|
||||
const dbPath = process.env['SQLITE_DB_PATH'] ?? ':memory:';
|
||||
client = new Database(dbPath);
|
||||
db = drizzle(client, { logger: ENABLE_LOGGING });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
client?.close();
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
});
|
||||
|
||||
test('migrator', async () => {
|
||||
db.run(sql`drop table if exists another_users`);
|
||||
db.run(sql`drop table if exists users12`);
|
||||
db.run(sql`drop table if exists __drizzle_migrations`);
|
||||
|
||||
migrate(db, { migrationsFolder: './drizzle2/sqlite' });
|
||||
|
||||
db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result = db.select().from(usersMigratorTable).all();
|
||||
|
||||
db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result2 = db.select().from(anotherUsersMigratorTable).all();
|
||||
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
expect(result2).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
db.run(sql`drop table another_users`);
|
||||
db.run(sql`drop table users12`);
|
||||
db.run(sql`drop table __drizzle_migrations`);
|
||||
});
|
||||
|
||||
skipTests([
|
||||
/**
|
||||
* doesn't work properly:
|
||||
* Expect: should rollback transaction and don't insert/ update data
|
||||
* Received: data inserted/ updated
|
||||
*/
|
||||
'transaction rollback',
|
||||
'nested transaction rollback',
|
||||
]);
|
||||
tests();
|
||||
@@ -0,0 +1,548 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
import 'dotenv/config';
|
||||
import { D1Database, D1DatabaseAPI } from '@miniflare/d1';
|
||||
import { createSQLiteDB } from '@miniflare/shared';
|
||||
import { eq, relations, sql } from 'drizzle-orm';
|
||||
import type { DrizzleD1Database } from 'drizzle-orm/d1';
|
||||
import { drizzle } from 'drizzle-orm/d1';
|
||||
import { type AnySQLiteColumn, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { afterAll, beforeAll, beforeEach, expect, expectTypeOf, test } from 'vitest';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
export const usersTable = sqliteTable('users', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
verified: integer('verified').notNull().default(0),
|
||||
invitedBy: integer('invited_by').references((): AnySQLiteColumn => usersTable.id),
|
||||
});
|
||||
export const usersConfig = relations(usersTable, ({ one, many }) => ({
|
||||
invitee: one(usersTable, {
|
||||
fields: [usersTable.invitedBy],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
usersToGroups: many(usersToGroupsTable),
|
||||
posts: many(postsTable),
|
||||
}));
|
||||
|
||||
export const groupsTable = sqliteTable('groups', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
});
|
||||
export const groupsConfig = relations(groupsTable, ({ many }) => ({
|
||||
usersToGroups: many(usersToGroupsTable),
|
||||
}));
|
||||
|
||||
export const usersToGroupsTable = sqliteTable(
|
||||
'users_to_groups',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id', { mode: 'number' }).notNull().references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
groupId: integer('group_id', { mode: 'number' }).notNull().references(
|
||||
() => groupsTable.id,
|
||||
),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.userId, t.groupId] }),
|
||||
}),
|
||||
);
|
||||
export const usersToGroupsConfig = relations(usersToGroupsTable, ({ one }) => ({
|
||||
group: one(groupsTable, {
|
||||
fields: [usersToGroupsTable.groupId],
|
||||
references: [groupsTable.id],
|
||||
}),
|
||||
user: one(usersTable, {
|
||||
fields: [usersToGroupsTable.userId],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const postsTable = sqliteTable('posts', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
content: text('content').notNull(),
|
||||
ownerId: integer('owner_id', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const postsConfig = relations(postsTable, ({ one, many }) => ({
|
||||
author: one(usersTable, {
|
||||
fields: [postsTable.ownerId],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
comments: many(commentsTable),
|
||||
}));
|
||||
|
||||
export const commentsTable = sqliteTable('comments', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
content: text('content').notNull(),
|
||||
creator: integer('creator', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
postId: integer('post_id', { mode: 'number' }).references(() => postsTable.id),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const commentsConfig = relations(commentsTable, ({ one, many }) => ({
|
||||
post: one(postsTable, {
|
||||
fields: [commentsTable.postId],
|
||||
references: [postsTable.id],
|
||||
}),
|
||||
author: one(usersTable, {
|
||||
fields: [commentsTable.creator],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
likes: many(commentLikesTable),
|
||||
}));
|
||||
|
||||
export const commentLikesTable = sqliteTable('comment_likes', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
creator: integer('creator', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
commentId: integer('comment_id', { mode: 'number' }).references(
|
||||
() => commentsTable.id,
|
||||
),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const commentLikesConfig = relations(commentLikesTable, ({ one }) => ({
|
||||
comment: one(commentsTable, {
|
||||
fields: [commentLikesTable.commentId],
|
||||
references: [commentsTable.id],
|
||||
}),
|
||||
author: one(usersTable, {
|
||||
fields: [commentLikesTable.creator],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = {
|
||||
usersTable,
|
||||
postsTable,
|
||||
commentsTable,
|
||||
usersToGroupsTable,
|
||||
groupsTable,
|
||||
commentLikesConfig,
|
||||
commentsConfig,
|
||||
postsConfig,
|
||||
usersToGroupsConfig,
|
||||
groupsConfig,
|
||||
usersConfig,
|
||||
};
|
||||
|
||||
let db: DrizzleD1Database<typeof schema>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const sqliteDb = await createSQLiteDB(':memory:');
|
||||
const d1db = new D1Database(new D1DatabaseAPI(sqliteDb));
|
||||
db = drizzle(d1db, { logger: ENABLE_LOGGING, schema });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.run(sql`drop table if exists \`groups\``);
|
||||
await db.run(sql`drop table if exists \`users\``);
|
||||
await db.run(sql`drop table if exists \`users_to_groups\``);
|
||||
await db.run(sql`drop table if exists \`posts\``);
|
||||
await db.run(sql`drop table if exists \`comments\``);
|
||||
await db.run(sql`drop table if exists \`comment_likes\``);
|
||||
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`users\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`name\` text NOT NULL,
|
||||
\`verified\` integer DEFAULT 0 NOT NULL,
|
||||
\`invited_by\` integer
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`groups\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`name\` text NOT NULL,
|
||||
\`description\` text
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`users_to_groups\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`user_id\` integer NOT NULL,
|
||||
\`group_id\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`posts\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`owner_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`comments\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`creator\` integer,
|
||||
\`post_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`comment_likes\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`creator\` integer,
|
||||
\`comment_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.run(sql`drop table if exists \`groups\``);
|
||||
await db.run(sql`drop table if exists \`users\``);
|
||||
await db.run(sql`drop table if exists \`users_to_groups\``);
|
||||
await db.run(sql`drop table if exists \`posts\``);
|
||||
await db.run(sql`drop table if exists \`comments\``);
|
||||
await db.run(sql`drop table if exists \`comment_likes\``);
|
||||
});
|
||||
|
||||
test('batch api example', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({
|
||||
id: usersTable.id,
|
||||
invitedBy: usersTable.invitedBy,
|
||||
}),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.select().from(usersTable),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
D1Result,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(3);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
invitedBy: null,
|
||||
}]);
|
||||
|
||||
// expect(batchResponse[1]).toEqual({
|
||||
// results: [],
|
||||
// success: true,
|
||||
// meta: {
|
||||
// duration: 0.027083873748779297,
|
||||
// last_row_id: 2,
|
||||
// changes: 1,
|
||||
// served_by: 'miniflare.db',
|
||||
// internal_stats: null,
|
||||
// },
|
||||
// });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api only relational many
|
||||
test('insert + findMany', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
D1Result,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(3);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
// expect(batchResponse[1]).toEqual({ columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api relational many + one
|
||||
test('insert + findMany + findFirst', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.query.usersTable.findFirst({}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
D1Result,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
} | undefined,
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
// expect(batchResponse[1]).toEqual({ columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual(
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
);
|
||||
});
|
||||
|
||||
test('insert + db.all + db.get + db.values + db.run', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.run(sql`insert into users (id, name) values (2, 'Dan')`),
|
||||
db.all<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
db.values(sql`select * from users`),
|
||||
db.get<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
D1Result,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
unknown[][],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
},
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(5);
|
||||
|
||||
expect(batchResponse[0], 'insert').toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
// expect(batchResponse[1]).toEqual({ columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2], 'all').toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invited_by: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3], 'values').toEqual([[1, 'John', 0, null], [2, 'Dan', 0, null]]);
|
||||
|
||||
expect(batchResponse[4], 'get').toEqual(
|
||||
{ id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
);
|
||||
});
|
||||
|
||||
// batch api combined rqb + raw call
|
||||
test('insert + findManyWith + db.all', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.all<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
D1Result,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
// expect(batchResponse[1]).toEqual({ columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invited_by: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api for insert + update + select
|
||||
test('insert + update + select + select partial', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.select().from(usersTable).where(eq(usersTable.id, 1)),
|
||||
db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from(usersTable),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
D1Result,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(5);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
// expect(batchResponse[1]).toEqual({ columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 1n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual([
|
||||
{ id: 1, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[4]).toEqual([
|
||||
{ id: 1, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api for insert + delete + select
|
||||
test('insert + delete + select + select partial', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.delete(usersTable).where(eq(usersTable.id, 1)).returning({ id: usersTable.id, invitedBy: usersTable.invitedBy }),
|
||||
db.query.usersTable.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
invitedBy: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
D1Result,
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
} | undefined,
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
// expect(batchResponse[1]).toEqual({ columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual(
|
||||
{ id: 2, invitedBy: null },
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { D1Database, D1DatabaseAPI } from '@miniflare/d1';
|
||||
import { createSQLiteDB } from '@miniflare/shared';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import type { DrizzleD1Database } from 'drizzle-orm/d1';
|
||||
import { drizzle } from 'drizzle-orm/d1';
|
||||
import { migrate } from 'drizzle-orm/d1/migrator';
|
||||
import { beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { randomString } from '~/utils';
|
||||
import { anotherUsersMigratorTable, tests, usersMigratorTable } from './sqlite-common';
|
||||
import { TestCache, TestGlobalCache, tests as cacheTests } from './sqlite-common-cache';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
let db: DrizzleD1Database;
|
||||
let dbGlobalCached: DrizzleD1Database;
|
||||
let cachedDb: DrizzleD1Database;
|
||||
|
||||
beforeAll(async () => {
|
||||
const sqliteDb = await createSQLiteDB(':memory:');
|
||||
const d1db = new D1Database(new D1DatabaseAPI(sqliteDb));
|
||||
db = drizzle(d1db, { logger: ENABLE_LOGGING });
|
||||
cachedDb = drizzle(d1db, { logger: ENABLE_LOGGING, cache: new TestCache() });
|
||||
dbGlobalCached = drizzle(d1db, { logger: ENABLE_LOGGING, cache: new TestGlobalCache() });
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
ctx.cachedSqlite = {
|
||||
db: cachedDb,
|
||||
dbGlobalCached,
|
||||
};
|
||||
});
|
||||
|
||||
test('migrator', async () => {
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists __drizzle_migrations`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite' });
|
||||
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result = await db.select().from(usersMigratorTable).all();
|
||||
|
||||
await db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result2 = await db.select().from(anotherUsersMigratorTable).all();
|
||||
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
expect(result2).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table __drizzle_migrations`);
|
||||
});
|
||||
|
||||
test('migrator : migrate with custom table', async () => {
|
||||
const customTable = randomString();
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists ${sql.identifier(customTable)}`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite', migrationsTable: customTable });
|
||||
|
||||
// test if the custom migrations table was created
|
||||
const res = await db.all(sql`select * from ${sql.identifier(customTable)};`);
|
||||
expect(res.length > 0).toBeTruthy();
|
||||
|
||||
// test if the migrated table are working as expected
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' });
|
||||
const result = await db.select().from(usersMigratorTable);
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table ${sql.identifier(customTable)}`);
|
||||
});
|
||||
|
||||
skipTests([
|
||||
// Cannot convert 49,50,55 to a BigInt
|
||||
'insert bigint values',
|
||||
// SyntaxError: Unexpected token , in JSON at position 2
|
||||
'json insert',
|
||||
'insert many',
|
||||
'insert many with returning',
|
||||
/**
|
||||
* TODO: Fix Bug! The objects should be equal
|
||||
*
|
||||
* See #528 for more details.
|
||||
* Tldr the D1 driver does not execute joins successfully
|
||||
*/
|
||||
'partial join with alias',
|
||||
'full join with alias',
|
||||
'select from alias',
|
||||
'join view as subquery',
|
||||
'cross join',
|
||||
]);
|
||||
cacheTests();
|
||||
tests();
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE `another_users` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`email` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `users12` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`email` text NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "66be869a-d55d-4790-a382-de654dff1506",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"another_users": {
|
||||
"name": "another_users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users12": {
|
||||
"name": "users12",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1732696446109,
|
||||
"tag": "0000_cuddly_black_bolt",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import m0000 from './0000_cuddly_black_bolt.sql';
|
||||
import journal from './meta/_journal.json';
|
||||
|
||||
export default {
|
||||
journal,
|
||||
migrations: {
|
||||
m0000,
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
// Generated by Wrangler by running `wrangler types`
|
||||
|
||||
interface Env {
|
||||
MY_DURABLE_OBJECT: DurableObjectNamespace<import('.').MyDurableObject>;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#:schema node_modules/wrangler/config-schema.json
|
||||
name = "sqlite-durable-objects"
|
||||
main = "index.ts"
|
||||
compatibility_date = "2024-11-12"
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
|
||||
# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.
|
||||
# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps.
|
||||
# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects
|
||||
[[durable_objects.bindings]]
|
||||
name = "MY_DURABLE_OBJECT"
|
||||
class_name = "MyDurableObject"
|
||||
|
||||
# Durable Object migrations.
|
||||
# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations
|
||||
[[migrations]]
|
||||
tag = "v1"
|
||||
new_sqlite_classes = ["MyDurableObject"]
|
||||
|
||||
[[rules]]
|
||||
type = "Text"
|
||||
globs = ["**/*.sql"]
|
||||
fallthrough = true
|
||||
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
import { createClient } from '@libsql/client';
|
||||
import type { Client, ResultSet } from '@libsql/client';
|
||||
import retry from 'async-retry';
|
||||
import { eq, relations, sql } from 'drizzle-orm';
|
||||
import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core';
|
||||
import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { afterAll, beforeAll, beforeEach, expect, expectTypeOf, test } from 'vitest';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
export const usersTable = sqliteTable('users', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
verified: integer('verified').notNull().default(0),
|
||||
invitedBy: integer('invited_by').references((): AnySQLiteColumn => usersTable.id),
|
||||
});
|
||||
export const usersConfig = relations(usersTable, ({ one, many }) => ({
|
||||
invitee: one(usersTable, {
|
||||
fields: [usersTable.invitedBy],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
usersToGroups: many(usersToGroupsTable),
|
||||
posts: many(postsTable),
|
||||
}));
|
||||
|
||||
export const groupsTable = sqliteTable('groups', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
});
|
||||
export const groupsConfig = relations(groupsTable, ({ many }) => ({
|
||||
usersToGroups: many(usersToGroupsTable),
|
||||
}));
|
||||
|
||||
export const usersToGroupsTable = sqliteTable(
|
||||
'users_to_groups',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id', { mode: 'number' }).notNull().references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
groupId: integer('group_id', { mode: 'number' }).notNull().references(
|
||||
() => groupsTable.id,
|
||||
),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.userId, t.groupId] }),
|
||||
}),
|
||||
);
|
||||
export const usersToGroupsConfig = relations(usersToGroupsTable, ({ one }) => ({
|
||||
group: one(groupsTable, {
|
||||
fields: [usersToGroupsTable.groupId],
|
||||
references: [groupsTable.id],
|
||||
}),
|
||||
user: one(usersTable, {
|
||||
fields: [usersToGroupsTable.userId],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const postsTable = sqliteTable('posts', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
content: text('content').notNull(),
|
||||
ownerId: integer('owner_id', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const postsConfig = relations(postsTable, ({ one, many }) => ({
|
||||
author: one(usersTable, {
|
||||
fields: [postsTable.ownerId],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
comments: many(commentsTable),
|
||||
}));
|
||||
|
||||
export const commentsTable = sqliteTable('comments', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
content: text('content').notNull(),
|
||||
creator: integer('creator', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
postId: integer('post_id', { mode: 'number' }).references(() => postsTable.id),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const commentsConfig = relations(commentsTable, ({ one, many }) => ({
|
||||
post: one(postsTable, {
|
||||
fields: [commentsTable.postId],
|
||||
references: [postsTable.id],
|
||||
}),
|
||||
author: one(usersTable, {
|
||||
fields: [commentsTable.creator],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
likes: many(commentLikesTable),
|
||||
}));
|
||||
|
||||
export const commentLikesTable = sqliteTable('comment_likes', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
creator: integer('creator', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
commentId: integer('comment_id', { mode: 'number' }).references(
|
||||
() => commentsTable.id,
|
||||
),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const commentLikesConfig = relations(commentLikesTable, ({ one }) => ({
|
||||
comment: one(commentsTable, {
|
||||
fields: [commentLikesTable.commentId],
|
||||
references: [commentsTable.id],
|
||||
}),
|
||||
author: one(usersTable, {
|
||||
fields: [commentLikesTable.creator],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = {
|
||||
usersTable,
|
||||
postsTable,
|
||||
commentsTable,
|
||||
usersToGroupsTable,
|
||||
groupsTable,
|
||||
commentLikesConfig,
|
||||
commentsConfig,
|
||||
postsConfig,
|
||||
usersToGroupsConfig,
|
||||
groupsConfig,
|
||||
usersConfig,
|
||||
};
|
||||
|
||||
let db: LibSQLDatabase<typeof schema>;
|
||||
let client: Client;
|
||||
|
||||
beforeAll(async () => {
|
||||
const url = process.env['LIBSQL_URL'];
|
||||
const authToken = process.env['LIBSQL_AUTH_TOKEN'];
|
||||
if (!url) {
|
||||
throw new Error('LIBSQL_URL is not set');
|
||||
}
|
||||
client = await retry(async () => {
|
||||
client = createClient({ url, authToken });
|
||||
return client;
|
||||
}, {
|
||||
retries: 20,
|
||||
factor: 1,
|
||||
minTimeout: 250,
|
||||
maxTimeout: 250,
|
||||
randomize: false,
|
||||
onRetry() {
|
||||
client?.close();
|
||||
},
|
||||
});
|
||||
db = drizzle(client, { schema, logger: ENABLE_LOGGING });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// client?.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.run(sql`drop table if exists \`groups\``);
|
||||
await db.run(sql`drop table if exists \`users\``);
|
||||
await db.run(sql`drop table if exists \`users_to_groups\``);
|
||||
await db.run(sql`drop table if exists \`posts\``);
|
||||
await db.run(sql`drop table if exists \`comments\``);
|
||||
await db.run(sql`drop table if exists \`comment_likes\``);
|
||||
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`users\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`name\` text NOT NULL,
|
||||
\`verified\` integer DEFAULT 0 NOT NULL,
|
||||
\`invited_by\` integer
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`groups\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`name\` text NOT NULL,
|
||||
\`description\` text
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`users_to_groups\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`user_id\` integer NOT NULL,
|
||||
\`group_id\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`posts\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`owner_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`comments\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`creator\` integer,
|
||||
\`post_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`comment_likes\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`creator\` integer,
|
||||
\`comment_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.run(sql`drop table if exists \`groups\``);
|
||||
await db.run(sql`drop table if exists \`users\``);
|
||||
await db.run(sql`drop table if exists \`users_to_groups\``);
|
||||
await db.run(sql`drop table if exists \`posts\``);
|
||||
await db.run(sql`drop table if exists \`comments\``);
|
||||
await db.run(sql`drop table if exists \`comment_likes\``);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('batch api example', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({
|
||||
id: usersTable.id,
|
||||
invitedBy: usersTable.invitedBy,
|
||||
}),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.select().from(usersTable),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
ResultSet,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(3);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
invitedBy: null,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ columnTypes: [], columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api only relational many
|
||||
test('insert + findMany', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
ResultSet,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(3);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ columnTypes: [], columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api relational many + one
|
||||
test('insert + findMany + findFirst', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.query.usersTable.findFirst({}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
ResultSet,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
} | undefined,
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ columnTypes: [], columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual(
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
);
|
||||
});
|
||||
|
||||
test('insert + db.all + db.get + db.values + db.run', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.run(sql`insert into users (id, name) values (2, 'Dan')`),
|
||||
db.all<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
db.values(sql`select * from users`),
|
||||
db.get<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
ResultSet,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
unknown[][],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
},
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(5);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ columnTypes: [], columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invited_by: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3].map((row) => Array.prototype.slice.call(row))).toEqual([
|
||||
[1, 'John', 0, null],
|
||||
[2, 'Dan', 0, null],
|
||||
]);
|
||||
|
||||
expect(batchResponse[4]).toEqual(
|
||||
{ id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
);
|
||||
});
|
||||
|
||||
// batch api combined rqb + raw call
|
||||
test('insert + findManyWith + db.all', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.all<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
ResultSet,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ columnTypes: [], columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invited_by: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api for insert + update + select
|
||||
test('insert + update + select + select partial', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.select().from(usersTable).where(eq(usersTable.id, 1)),
|
||||
db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from(usersTable),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
ResultSet,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(5);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ columnTypes: [], columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 1n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual([
|
||||
{ id: 1, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[4]).toEqual([
|
||||
{ id: 1, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api for insert + delete + select
|
||||
test('insert + delete + select + select partial', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.delete(usersTable).where(eq(usersTable.id, 1)).returning({ id: usersTable.id, invitedBy: usersTable.invitedBy }),
|
||||
db.query.usersTable.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
invitedBy: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
ResultSet,
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
} | undefined,
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ columnTypes: [], columns: [], rows: [], rowsAffected: 1, lastInsertRowid: 2n });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual(
|
||||
{ id: 2, invitedBy: null },
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import { type Client, createClient } from '@libsql/client/http';
|
||||
import retry from 'async-retry';
|
||||
import { asc, eq, getTableColumns, sql } from 'drizzle-orm';
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import { drizzle } from 'drizzle-orm/libsql/http';
|
||||
import { migrate } from 'drizzle-orm/libsql/migrator';
|
||||
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { randomString } from '~/utils';
|
||||
import { anotherUsersMigratorTable, tests, usersMigratorTable, usersOnUpdate } from './sqlite-common';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
let db: LibSQLDatabase;
|
||||
let client: Client;
|
||||
|
||||
beforeAll(async () => {
|
||||
const url = process.env['LIBSQL_REMOTE_URL'];
|
||||
const authToken = process.env['LIBSQL_REMOTE_TOKEN'];
|
||||
if (!url) {
|
||||
throw new Error('LIBSQL_REMOTE_URL is not set');
|
||||
}
|
||||
client = await retry(async () => {
|
||||
client = createClient({ url, authToken });
|
||||
return client;
|
||||
}, {
|
||||
retries: 20,
|
||||
factor: 1,
|
||||
minTimeout: 250,
|
||||
maxTimeout: 250,
|
||||
randomize: false,
|
||||
onRetry() {
|
||||
client?.close();
|
||||
},
|
||||
});
|
||||
db = drizzle(client, { logger: ENABLE_LOGGING });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
client?.close();
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
});
|
||||
|
||||
test('migrator', async () => {
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists __drizzle_migrations`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite' });
|
||||
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result = await db.select().from(usersMigratorTable).all();
|
||||
|
||||
await db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result2 = await db.select().from(anotherUsersMigratorTable).all();
|
||||
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
expect(result2).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table __drizzle_migrations`);
|
||||
});
|
||||
|
||||
test('migrator : migrate with custom table', async () => {
|
||||
const customTable = randomString();
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists ${sql.identifier(customTable)}`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite', migrationsTable: customTable });
|
||||
|
||||
// test if the custom migrations table was created
|
||||
const res = await db.all(sql`select * from ${sql.identifier(customTable)};`);
|
||||
expect(res.length > 0).toBeTruthy();
|
||||
|
||||
// test if the migrated table are working as expected
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' });
|
||||
const result = await db.select().from(usersMigratorTable);
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table ${sql.identifier(customTable)}`);
|
||||
});
|
||||
|
||||
test('test $onUpdateFn and $onUpdate works as $default', async (ctx) => {
|
||||
const { db } = ctx.sqlite;
|
||||
|
||||
await db.run(sql`drop table if exists ${usersOnUpdate}`);
|
||||
|
||||
await db.run(
|
||||
sql`
|
||||
create table ${usersOnUpdate} (
|
||||
id integer primary key autoincrement,
|
||||
name text not null,
|
||||
update_counter integer default 1 not null,
|
||||
updated_at integer,
|
||||
always_null text
|
||||
)
|
||||
`,
|
||||
);
|
||||
|
||||
await db.insert(usersOnUpdate).values([
|
||||
{ name: 'John' },
|
||||
{ name: 'Jane' },
|
||||
{ name: 'Jack' },
|
||||
{ name: 'Jill' },
|
||||
]);
|
||||
const { updatedAt, ...rest } = getTableColumns(usersOnUpdate);
|
||||
|
||||
const justDates = await db.select({ updatedAt }).from(usersOnUpdate).orderBy(asc(usersOnUpdate.id));
|
||||
|
||||
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 = 1750;
|
||||
|
||||
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.sqlite;
|
||||
|
||||
await db.run(sql`drop table if exists ${usersOnUpdate}`);
|
||||
|
||||
await db.run(
|
||||
sql`
|
||||
create table ${usersOnUpdate} (
|
||||
id integer primary key autoincrement,
|
||||
name text not null,
|
||||
update_counter integer default 1,
|
||||
updated_at integer,
|
||||
always_null text
|
||||
)
|
||||
`,
|
||||
);
|
||||
|
||||
await db.insert(usersOnUpdate).values([
|
||||
{ name: 'John', alwaysNull: 'this will be null after updating' },
|
||||
{ name: 'Jane' },
|
||||
{ name: 'Jack' },
|
||||
{ name: 'Jill' },
|
||||
]);
|
||||
const { updatedAt, ...rest } = getTableColumns(usersOnUpdate);
|
||||
|
||||
await db.update(usersOnUpdate).set({ name: 'Angel' }).where(eq(usersOnUpdate.id, 1));
|
||||
await db.update(usersOnUpdate).set({ updateCounter: null }).where(eq(usersOnUpdate.id, 2));
|
||||
|
||||
const justDates = await db.select({ updatedAt }).from(usersOnUpdate).orderBy(asc(usersOnUpdate.id));
|
||||
|
||||
const response = await db.select({ ...rest }).from(usersOnUpdate).orderBy(asc(usersOnUpdate.id));
|
||||
|
||||
expect(response).toEqual([
|
||||
{ name: 'Angel', id: 1, updateCounter: 2, alwaysNull: null },
|
||||
{ name: 'Jane', id: 2, updateCounter: null, alwaysNull: null },
|
||||
{ name: 'Jack', id: 3, updateCounter: 1, alwaysNull: null },
|
||||
{ name: 'Jill', id: 4, updateCounter: 1, alwaysNull: null },
|
||||
]);
|
||||
const msDelay = 1750;
|
||||
|
||||
for (const eachUser of justDates) {
|
||||
expect(eachUser.updatedAt!.valueOf()).toBeGreaterThan(Date.now() - msDelay);
|
||||
}
|
||||
});
|
||||
|
||||
skipTests([
|
||||
'delete with limit and order by',
|
||||
'update with limit and order by',
|
||||
'test $onUpdateFn and $onUpdate works as $default',
|
||||
'test $onUpdateFn and $onUpdate works updating',
|
||||
]);
|
||||
|
||||
tests();
|
||||
@@ -0,0 +1,97 @@
|
||||
import { type Client, createClient } from '@libsql/client/node';
|
||||
import retry from 'async-retry';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import { migrate } from 'drizzle-orm/libsql/migrator';
|
||||
import { drizzle } from 'drizzle-orm/libsql/node';
|
||||
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { randomString } from '~/utils';
|
||||
import { anotherUsersMigratorTable, tests, usersMigratorTable } from './sqlite-common';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
let db: LibSQLDatabase;
|
||||
let client: Client;
|
||||
|
||||
beforeAll(async () => {
|
||||
const url = process.env['LIBSQL_URL'];
|
||||
const authToken = process.env['LIBSQL_AUTH_TOKEN'];
|
||||
if (!url) {
|
||||
throw new Error('LIBSQL_URL is not set');
|
||||
}
|
||||
client = await retry(async () => {
|
||||
client = createClient({ url, authToken });
|
||||
return client;
|
||||
}, {
|
||||
retries: 20,
|
||||
factor: 1,
|
||||
minTimeout: 250,
|
||||
maxTimeout: 250,
|
||||
randomize: false,
|
||||
onRetry() {
|
||||
client?.close();
|
||||
},
|
||||
});
|
||||
db = drizzle(client, { logger: ENABLE_LOGGING });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
client?.close();
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
});
|
||||
|
||||
test('migrator', async () => {
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists __drizzle_migrations`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite' });
|
||||
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result = await db.select().from(usersMigratorTable).all();
|
||||
|
||||
await db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result2 = await db.select().from(anotherUsersMigratorTable).all();
|
||||
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
expect(result2).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table __drizzle_migrations`);
|
||||
});
|
||||
|
||||
test('migrator : migrate with custom table', async () => {
|
||||
const customTable = randomString();
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists ${sql.identifier(customTable)}`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite', migrationsTable: customTable });
|
||||
|
||||
// test if the custom migrations table was created
|
||||
const res = await db.all(sql`select * from ${sql.identifier(customTable)};`);
|
||||
expect(res.length > 0).toBeTruthy();
|
||||
|
||||
// test if the migrated table are working as expected
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' });
|
||||
const result = await db.select().from(usersMigratorTable);
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table ${sql.identifier(customTable)}`);
|
||||
});
|
||||
|
||||
skipTests([
|
||||
'delete with limit and order by',
|
||||
'update with limit and order by',
|
||||
]);
|
||||
|
||||
tests();
|
||||
@@ -0,0 +1,97 @@
|
||||
import { type Client, createClient } from '@libsql/client/sqlite3';
|
||||
import retry from 'async-retry';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import { migrate } from 'drizzle-orm/libsql/migrator';
|
||||
import { drizzle } from 'drizzle-orm/libsql/sqlite3';
|
||||
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { randomString } from '~/utils';
|
||||
import { anotherUsersMigratorTable, tests, usersMigratorTable } from './sqlite-common';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
let db: LibSQLDatabase;
|
||||
let client: Client;
|
||||
|
||||
beforeAll(async () => {
|
||||
const url = ':memory:';
|
||||
client = await retry(async () => {
|
||||
client = createClient({ url });
|
||||
return client;
|
||||
}, {
|
||||
retries: 20,
|
||||
factor: 1,
|
||||
minTimeout: 250,
|
||||
maxTimeout: 250,
|
||||
randomize: false,
|
||||
onRetry() {
|
||||
client?.close();
|
||||
},
|
||||
});
|
||||
db = drizzle(client, { logger: ENABLE_LOGGING });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
client?.close();
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
});
|
||||
|
||||
test('migrator', async () => {
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists __drizzle_migrations`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite' });
|
||||
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result = await db.select().from(usersMigratorTable).all();
|
||||
|
||||
await db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result2 = await db.select().from(anotherUsersMigratorTable).all();
|
||||
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
expect(result2).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table __drizzle_migrations`);
|
||||
});
|
||||
|
||||
test('migrator : migrate with custom table', async () => {
|
||||
const customTable = randomString();
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists ${sql.identifier(customTable)}`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite', migrationsTable: customTable });
|
||||
|
||||
// test if the custom migrations table was created
|
||||
const res = await db.all(sql`select * from ${sql.identifier(customTable)};`);
|
||||
expect(res.length > 0).toBeTruthy();
|
||||
|
||||
// test if the migrated table are working as expected
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' });
|
||||
const result = await db.select().from(usersMigratorTable);
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table ${sql.identifier(customTable)}`);
|
||||
});
|
||||
|
||||
skipTests([
|
||||
'delete with limit and order by',
|
||||
'update with limit and order by',
|
||||
'transaction',
|
||||
'transaction rollback',
|
||||
'nested transaction',
|
||||
'nested transaction rollback',
|
||||
]);
|
||||
|
||||
tests();
|
||||
@@ -0,0 +1,188 @@
|
||||
import { type Client, createClient } from '@libsql/client/ws';
|
||||
import retry from 'async-retry';
|
||||
import { asc, eq, getTableColumns, sql } from 'drizzle-orm';
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import { migrate } from 'drizzle-orm/libsql/migrator';
|
||||
import { drizzle } from 'drizzle-orm/libsql/ws';
|
||||
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { randomString } from '~/utils';
|
||||
import { anotherUsersMigratorTable, tests, usersMigratorTable, usersOnUpdate } from './sqlite-common';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
let db: LibSQLDatabase;
|
||||
let client: Client;
|
||||
|
||||
beforeAll(async () => {
|
||||
const url = process.env['LIBSQL_REMOTE_URL'];
|
||||
const authToken = process.env['LIBSQL_REMOTE_TOKEN'];
|
||||
if (!url) {
|
||||
throw new Error('LIBSQL_REMOTE_URL is not set');
|
||||
}
|
||||
client = await retry(async () => {
|
||||
client = createClient({ url, authToken });
|
||||
return client;
|
||||
}, {
|
||||
retries: 20,
|
||||
factor: 1,
|
||||
minTimeout: 250,
|
||||
maxTimeout: 250,
|
||||
randomize: false,
|
||||
onRetry() {
|
||||
client?.close();
|
||||
},
|
||||
});
|
||||
db = drizzle(client, { logger: ENABLE_LOGGING });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
client?.close();
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
});
|
||||
|
||||
test('migrator', async () => {
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists __drizzle_migrations`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite' });
|
||||
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result = await db.select().from(usersMigratorTable).all();
|
||||
|
||||
await db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result2 = await db.select().from(anotherUsersMigratorTable).all();
|
||||
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
expect(result2).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table __drizzle_migrations`);
|
||||
});
|
||||
|
||||
test('migrator : migrate with custom table', async () => {
|
||||
const customTable = randomString();
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists ${sql.identifier(customTable)}`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite', migrationsTable: customTable });
|
||||
|
||||
// test if the custom migrations table was created
|
||||
const res = await db.all(sql`select * from ${sql.identifier(customTable)};`);
|
||||
expect(res.length > 0).toBeTruthy();
|
||||
|
||||
// test if the migrated table are working as expected
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' });
|
||||
const result = await db.select().from(usersMigratorTable);
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table ${sql.identifier(customTable)}`);
|
||||
});
|
||||
|
||||
test('test $onUpdateFn and $onUpdate works as $default', async (ctx) => {
|
||||
const { db } = ctx.sqlite;
|
||||
|
||||
await db.run(sql`drop table if exists ${usersOnUpdate}`);
|
||||
|
||||
await db.run(
|
||||
sql`
|
||||
create table ${usersOnUpdate} (
|
||||
id integer primary key autoincrement,
|
||||
name text not null,
|
||||
update_counter integer default 1 not null,
|
||||
updated_at integer,
|
||||
always_null text
|
||||
)
|
||||
`,
|
||||
);
|
||||
|
||||
await db.insert(usersOnUpdate).values([
|
||||
{ name: 'John' },
|
||||
{ name: 'Jane' },
|
||||
{ name: 'Jack' },
|
||||
{ name: 'Jill' },
|
||||
]);
|
||||
const { updatedAt, ...rest } = getTableColumns(usersOnUpdate);
|
||||
|
||||
const justDates = await db.select({ updatedAt }).from(usersOnUpdate).orderBy(asc(usersOnUpdate.id));
|
||||
|
||||
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 = 1250;
|
||||
|
||||
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.sqlite;
|
||||
|
||||
await db.run(sql`drop table if exists ${usersOnUpdate}`);
|
||||
|
||||
await db.run(
|
||||
sql`
|
||||
create table ${usersOnUpdate} (
|
||||
id integer primary key autoincrement,
|
||||
name text not null,
|
||||
update_counter integer default 1,
|
||||
updated_at integer,
|
||||
always_null text
|
||||
)
|
||||
`,
|
||||
);
|
||||
|
||||
await db.insert(usersOnUpdate).values([
|
||||
{ name: 'John', alwaysNull: 'this will be null after updating' },
|
||||
{ name: 'Jane' },
|
||||
{ name: 'Jack' },
|
||||
{ name: 'Jill' },
|
||||
]);
|
||||
const { updatedAt, ...rest } = getTableColumns(usersOnUpdate);
|
||||
|
||||
await db.update(usersOnUpdate).set({ name: 'Angel' }).where(eq(usersOnUpdate.id, 1));
|
||||
await db.update(usersOnUpdate).set({ updateCounter: null }).where(eq(usersOnUpdate.id, 2));
|
||||
|
||||
const justDates = await db.select({ updatedAt }).from(usersOnUpdate).orderBy(asc(usersOnUpdate.id));
|
||||
|
||||
const response = await db.select({ ...rest }).from(usersOnUpdate).orderBy(asc(usersOnUpdate.id));
|
||||
|
||||
expect(response).toEqual([
|
||||
{ name: 'Angel', id: 1, updateCounter: 2, alwaysNull: null },
|
||||
{ name: 'Jane', id: 2, updateCounter: null, alwaysNull: null },
|
||||
{ name: 'Jack', id: 3, updateCounter: 1, alwaysNull: null },
|
||||
{ name: 'Jill', id: 4, updateCounter: 1, alwaysNull: null },
|
||||
]);
|
||||
const msDelay = 1250;
|
||||
|
||||
for (const eachUser of justDates) {
|
||||
expect(eachUser.updatedAt!.valueOf()).toBeGreaterThan(Date.now() - msDelay);
|
||||
}
|
||||
});
|
||||
|
||||
skipTests([
|
||||
'delete with limit and order by',
|
||||
'update with limit and order by',
|
||||
'join view as subquery',
|
||||
'test $onUpdateFn and $onUpdate works as $default',
|
||||
'test $onUpdateFn and $onUpdate works updating',
|
||||
'prepared statement reuse',
|
||||
]);
|
||||
|
||||
tests();
|
||||
@@ -0,0 +1,106 @@
|
||||
import { type Client, createClient } from '@libsql/client';
|
||||
import retry from 'async-retry';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import { migrate } from 'drizzle-orm/libsql/migrator';
|
||||
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { randomString } from '~/utils';
|
||||
import { anotherUsersMigratorTable, tests, usersMigratorTable } from './sqlite-common';
|
||||
import { TestCache, TestGlobalCache, tests as cacheTests } from './sqlite-common-cache';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
let db: LibSQLDatabase;
|
||||
let dbGlobalCached: LibSQLDatabase;
|
||||
let cachedDb: LibSQLDatabase;
|
||||
let client: Client;
|
||||
|
||||
beforeAll(async () => {
|
||||
const url = process.env['LIBSQL_URL'];
|
||||
const authToken = process.env['LIBSQL_AUTH_TOKEN'];
|
||||
if (!url) {
|
||||
throw new Error('LIBSQL_URL is not set');
|
||||
}
|
||||
client = await retry(async () => {
|
||||
client = createClient({ url, authToken });
|
||||
return client;
|
||||
}, {
|
||||
retries: 20,
|
||||
factor: 1,
|
||||
minTimeout: 250,
|
||||
maxTimeout: 250,
|
||||
randomize: false,
|
||||
onRetry() {
|
||||
client?.close();
|
||||
},
|
||||
});
|
||||
db = drizzle(client, { logger: ENABLE_LOGGING });
|
||||
cachedDb = drizzle(client, { logger: ENABLE_LOGGING, cache: new TestCache() });
|
||||
dbGlobalCached = drizzle(client, { logger: ENABLE_LOGGING, cache: new TestGlobalCache() });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
client?.close();
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
ctx.cachedSqlite = {
|
||||
db: cachedDb,
|
||||
dbGlobalCached,
|
||||
};
|
||||
});
|
||||
|
||||
test('migrator', async () => {
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists __drizzle_migrations`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite' });
|
||||
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result = await db.select().from(usersMigratorTable).all();
|
||||
|
||||
await db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result2 = await db.select().from(anotherUsersMigratorTable).all();
|
||||
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
expect(result2).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table __drizzle_migrations`);
|
||||
});
|
||||
|
||||
test('migrator : migrate with custom table', async () => {
|
||||
const customTable = randomString();
|
||||
await db.run(sql`drop table if exists another_users`);
|
||||
await db.run(sql`drop table if exists users12`);
|
||||
await db.run(sql`drop table if exists ${sql.identifier(customTable)}`);
|
||||
|
||||
await migrate(db, { migrationsFolder: './drizzle2/sqlite', migrationsTable: customTable });
|
||||
|
||||
// test if the custom migrations table was created
|
||||
const res = await db.all(sql`select * from ${sql.identifier(customTable)};`);
|
||||
expect(res.length > 0).toBeTruthy();
|
||||
|
||||
// test if the migrated table are working as expected
|
||||
await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' });
|
||||
const result = await db.select().from(usersMigratorTable);
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
await db.run(sql`drop table another_users`);
|
||||
await db.run(sql`drop table users12`);
|
||||
await db.run(sql`drop table ${sql.identifier(customTable)}`);
|
||||
});
|
||||
|
||||
skipTests([
|
||||
'delete with limit and order by',
|
||||
'update with limit and order by',
|
||||
]);
|
||||
|
||||
cacheTests();
|
||||
tests();
|
||||
@@ -0,0 +1,64 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import type { SQLJsDatabase } from 'drizzle-orm/sql-js';
|
||||
import { drizzle } from 'drizzle-orm/sql-js';
|
||||
import { migrate } from 'drizzle-orm/sql-js/migrator';
|
||||
import type { Database } from 'sql.js';
|
||||
import initSqlJs from 'sql.js';
|
||||
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { anotherUsersMigratorTable, tests, usersMigratorTable } from './sqlite-common';
|
||||
|
||||
const ENABLE_LOGGING = false;
|
||||
|
||||
let db: SQLJsDatabase;
|
||||
let client: Database;
|
||||
|
||||
beforeAll(async () => {
|
||||
const SQL = await initSqlJs();
|
||||
client = new SQL.Database();
|
||||
db = drizzle(client, { logger: ENABLE_LOGGING });
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
client?.close();
|
||||
});
|
||||
|
||||
test('migrator', async () => {
|
||||
db.run(sql`drop table if exists another_users`);
|
||||
db.run(sql`drop table if exists users12`);
|
||||
db.run(sql`drop table if exists __drizzle_migrations`);
|
||||
|
||||
migrate(db, { migrationsFolder: './drizzle2/sqlite' });
|
||||
|
||||
db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result = db.select().from(usersMigratorTable).all();
|
||||
|
||||
db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run();
|
||||
const result2 = db.select().from(anotherUsersMigratorTable).all();
|
||||
|
||||
expect(result).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
expect(result2).toEqual([{ id: 1, name: 'John', email: 'email' }]);
|
||||
|
||||
db.run(sql`drop table another_users`);
|
||||
db.run(sql`drop table users12`);
|
||||
db.run(sql`drop table __drizzle_migrations`);
|
||||
});
|
||||
|
||||
skipTests([
|
||||
/**
|
||||
* doesn't work properly:
|
||||
* Expect: should rollback transaction and don't insert/ update data
|
||||
* Received: data inserted/ updated
|
||||
*/
|
||||
'transaction rollback',
|
||||
'nested transaction rollback',
|
||||
'delete with limit and order by',
|
||||
'update with limit and order by',
|
||||
]);
|
||||
tests();
|
||||
@@ -0,0 +1,380 @@
|
||||
import { eq, getTableName, is, sql, Table } from 'drizzle-orm';
|
||||
import type { MutationOption } from 'drizzle-orm/cache/core';
|
||||
import { Cache } from 'drizzle-orm/cache/core';
|
||||
import type { CacheConfig } from 'drizzle-orm/cache/core/types';
|
||||
import { alias, type BaseSQLiteDatabase, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import Keyv from 'keyv';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
// eslint-disable-next-line drizzle-internal/require-entity-kind
|
||||
export class TestGlobalCache extends Cache {
|
||||
private globalTtl: number = 1000;
|
||||
private usedTablesPerKey: Record<string, string[]> = {};
|
||||
|
||||
constructor(private kv: Keyv = new Keyv()) {
|
||||
super();
|
||||
}
|
||||
|
||||
override strategy(): 'explicit' | 'all' {
|
||||
return 'all';
|
||||
}
|
||||
override async get(key: string, _tables: string[], _isTag: boolean): Promise<any[] | undefined> {
|
||||
const res = await this.kv.get(key) ?? undefined;
|
||||
return res;
|
||||
}
|
||||
override async put(
|
||||
key: string,
|
||||
response: any,
|
||||
tables: string[],
|
||||
isTag: boolean,
|
||||
config?: CacheConfig,
|
||||
): Promise<void> {
|
||||
await this.kv.set(key, response, config ? config.ex : this.globalTtl);
|
||||
for (const table of tables) {
|
||||
const keys = this.usedTablesPerKey[table];
|
||||
if (keys === undefined) {
|
||||
this.usedTablesPerKey[table] = [key];
|
||||
} else {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
override async onMutate(params: MutationOption): Promise<void> {
|
||||
const tagsArray = params.tags ? Array.isArray(params.tags) ? params.tags : [params.tags] : [];
|
||||
const tablesArray = params.tables ? Array.isArray(params.tables) ? params.tables : [params.tables] : [];
|
||||
|
||||
const keysToDelete = new Set<string>();
|
||||
|
||||
for (const table of tablesArray) {
|
||||
const tableName = is(table, Table) ? getTableName(table) : table as string;
|
||||
const keys = this.usedTablesPerKey[tableName] ?? [];
|
||||
for (const key of keys) keysToDelete.add(key);
|
||||
}
|
||||
|
||||
if (keysToDelete.size > 0 || tagsArray.length > 0) {
|
||||
for (const tag of tagsArray) {
|
||||
await this.kv.delete(tag);
|
||||
}
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
await this.kv.delete(key);
|
||||
for (const table of tablesArray) {
|
||||
const tableName = is(table, Table) ? getTableName(table) : table as string;
|
||||
this.usedTablesPerKey[tableName] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line drizzle-internal/require-entity-kind
|
||||
export class TestCache extends TestGlobalCache {
|
||||
override strategy(): 'explicit' | 'all' {
|
||||
return 'explicit';
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
interface TestContext {
|
||||
cachedSqlite: {
|
||||
db: BaseSQLiteDatabase<any, any>;
|
||||
dbGlobalCached: BaseSQLiteDatabase<any, any>;
|
||||
};
|
||||
sqlite: {
|
||||
db: BaseSQLiteDatabase<'async' | 'sync', any, Record<string, never>>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const usersTable = sqliteTable('users', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
verified: integer('verified', { mode: 'boolean' }).notNull().default(false),
|
||||
jsonb: text('jsonb', { mode: 'json' }).$type<string[]>(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }),
|
||||
});
|
||||
|
||||
const postsTable = sqliteTable('posts', {
|
||||
id: integer().primaryKey({ autoIncrement: true }),
|
||||
description: text().notNull(),
|
||||
userId: integer('user_id').references(() => usersTable.id),
|
||||
});
|
||||
|
||||
export function tests() {
|
||||
describe('common_cache', () => {
|
||||
beforeEach(async (ctx) => {
|
||||
const { db, dbGlobalCached } = ctx.cachedSqlite;
|
||||
await db.run(sql`drop table if exists users`);
|
||||
await db.run(sql`drop table if exists posts`);
|
||||
await db.$cache?.invalidate({ tables: 'users' });
|
||||
await dbGlobalCached.$cache?.invalidate({ tables: 'users' });
|
||||
// public users
|
||||
await db.run(
|
||||
sql`
|
||||
create table users (
|
||||
id integer primary key AUTOINCREMENT,
|
||||
name text not null,
|
||||
verified integer not null default 0,
|
||||
jsonb text,
|
||||
created_at integer
|
||||
)
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
create table posts (
|
||||
id integer primary key AUTOINCREMENT,
|
||||
description text not null,
|
||||
user_id int
|
||||
)
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
test('test force invalidate', async (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'invalidate');
|
||||
await db.$cache?.invalidate({ tables: 'users' });
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('default global config - no cache should be hit', async (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable);
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(0);
|
||||
expect(spyGet).toHaveBeenCalledTimes(0);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('default global config + enable cache on select: get, put', async (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable).$withCache();
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(1);
|
||||
expect(spyGet).toHaveBeenCalledTimes(1);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('default global config + enable cache on select + write: get, put, onMutate', async (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable).$withCache({ config: { ex: 1 } });
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(1);
|
||||
expect(spyGet).toHaveBeenCalledTimes(1);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
|
||||
spyPut.mockClear();
|
||||
spyGet.mockClear();
|
||||
spyInvalidate.mockClear();
|
||||
|
||||
await db.insert(usersTable).values({ name: 'John' });
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(0);
|
||||
expect(spyGet).toHaveBeenCalledTimes(0);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('default global config + enable cache on select + disable invalidate: get, put', async (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable).$withCache({ tag: 'custom', autoInvalidate: false, config: { ex: 1 } });
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(1);
|
||||
expect(spyGet).toHaveBeenCalledTimes(1);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
|
||||
await db.insert(usersTable).values({ name: 'John' });
|
||||
|
||||
// invalidate force
|
||||
await db.$cache?.invalidate({ tags: ['custom'] });
|
||||
});
|
||||
|
||||
test('global: true + disable cache', async (ctx) => {
|
||||
const { dbGlobalCached: db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable).$withCache(false);
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(0);
|
||||
expect(spyGet).toHaveBeenCalledTimes(0);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('global: true - cache should be hit', async (ctx) => {
|
||||
const { dbGlobalCached: db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable);
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(1);
|
||||
expect(spyGet).toHaveBeenCalledTimes(1);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('global: true - cache: false on select - no cache hit', async (ctx) => {
|
||||
const { dbGlobalCached: db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable).$withCache(false);
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(0);
|
||||
expect(spyGet).toHaveBeenCalledTimes(0);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('global: true - disable invalidate - cache hit + no invalidate', async (ctx) => {
|
||||
const { dbGlobalCached: db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable).$withCache({ autoInvalidate: false });
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(1);
|
||||
expect(spyGet).toHaveBeenCalledTimes(1);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
|
||||
spyPut.mockClear();
|
||||
spyGet.mockClear();
|
||||
spyInvalidate.mockClear();
|
||||
|
||||
await db.insert(usersTable).values({ name: 'John' });
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(0);
|
||||
expect(spyGet).toHaveBeenCalledTimes(0);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('global: true - with custom tag', async (ctx) => {
|
||||
const { dbGlobalCached: db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
const spyPut = vi.spyOn(db.$cache, 'put');
|
||||
// @ts-expect-error
|
||||
const spyGet = vi.spyOn(db.$cache, 'get');
|
||||
// @ts-expect-error
|
||||
const spyInvalidate = vi.spyOn(db.$cache, 'onMutate');
|
||||
|
||||
await db.select().from(usersTable).$withCache({ tag: 'custom', autoInvalidate: false });
|
||||
|
||||
expect(spyPut).toHaveBeenCalledTimes(1);
|
||||
expect(spyGet).toHaveBeenCalledTimes(1);
|
||||
expect(spyInvalidate).toHaveBeenCalledTimes(0);
|
||||
|
||||
await db.insert(usersTable).values({ name: 'John' });
|
||||
|
||||
// invalidate force
|
||||
await db.$cache?.invalidate({ tags: ['custom'] });
|
||||
});
|
||||
|
||||
// check select used tables
|
||||
test('check simple select used tables', (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
expect(db.select().from(usersTable).getUsedTables()).toStrictEqual(['users']);
|
||||
// @ts-expect-error
|
||||
expect(db.select().from(sql`${usersTable}`).getUsedTables()).toStrictEqual(['users']);
|
||||
});
|
||||
// check select+join used tables
|
||||
test('select+join', (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
// @ts-expect-error
|
||||
expect(db.select().from(usersTable).leftJoin(postsTable, eq(usersTable.id, postsTable.userId)).getUsedTables())
|
||||
.toStrictEqual(['users', 'posts']);
|
||||
expect(
|
||||
// @ts-expect-error
|
||||
db.select().from(sql`${usersTable}`).leftJoin(postsTable, eq(usersTable.id, postsTable.userId)).getUsedTables(),
|
||||
).toStrictEqual(['users', 'posts']);
|
||||
});
|
||||
// check select+2join used tables
|
||||
test('select+2joins', (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
expect(
|
||||
db.select().from(usersTable).leftJoin(
|
||||
postsTable,
|
||||
eq(usersTable.id, postsTable.userId),
|
||||
).leftJoin(
|
||||
alias(postsTable, 'post2'),
|
||||
eq(usersTable.id, postsTable.userId),
|
||||
)
|
||||
// @ts-expect-error
|
||||
.getUsedTables(),
|
||||
)
|
||||
.toStrictEqual(['users', 'posts']);
|
||||
expect(
|
||||
db.select().from(sql`${usersTable}`).leftJoin(postsTable, eq(usersTable.id, postsTable.userId)).leftJoin(
|
||||
alias(postsTable, 'post2'),
|
||||
eq(usersTable.id, postsTable.userId),
|
||||
// @ts-expect-error
|
||||
).getUsedTables(),
|
||||
).toStrictEqual(['users', 'posts']);
|
||||
});
|
||||
// select subquery used tables
|
||||
test('select+join', (ctx) => {
|
||||
const { db } = ctx.cachedSqlite;
|
||||
|
||||
const sq = db.select().from(usersTable).where(eq(usersTable.id, 42)).as('sq');
|
||||
|
||||
// @ts-expect-error
|
||||
expect(db.select().from(sq).getUsedTables()).toStrictEqual(['users']);
|
||||
});
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,700 @@
|
||||
/* eslint-disable drizzle-internal/require-entity-kind */
|
||||
import type BetterSqlite3 from 'better-sqlite3';
|
||||
import Database from 'better-sqlite3';
|
||||
import { eq, relations, sql } from 'drizzle-orm';
|
||||
import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core';
|
||||
import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import type { SqliteRemoteDatabase, SqliteRemoteResult } from 'drizzle-orm/sqlite-proxy';
|
||||
import { drizzle as proxyDrizzle } from 'drizzle-orm/sqlite-proxy';
|
||||
import { afterAll, beforeAll, beforeEach, expect, expectTypeOf, test } from 'vitest';
|
||||
|
||||
export const usersTable = sqliteTable('users', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
verified: integer('verified').notNull().default(0),
|
||||
invitedBy: integer('invited_by').references((): AnySQLiteColumn => usersTable.id),
|
||||
});
|
||||
export const usersConfig = relations(usersTable, ({ one, many }) => ({
|
||||
invitee: one(usersTable, {
|
||||
fields: [usersTable.invitedBy],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
usersToGroups: many(usersToGroupsTable),
|
||||
posts: many(postsTable),
|
||||
}));
|
||||
|
||||
export const groupsTable = sqliteTable('groups', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
});
|
||||
export const groupsConfig = relations(groupsTable, ({ many }) => ({
|
||||
usersToGroups: many(usersToGroupsTable),
|
||||
}));
|
||||
|
||||
export const usersToGroupsTable = sqliteTable(
|
||||
'users_to_groups',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id', { mode: 'number' }).notNull().references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
groupId: integer('group_id', { mode: 'number' }).notNull().references(
|
||||
() => groupsTable.id,
|
||||
),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.userId, t.groupId] }),
|
||||
}),
|
||||
);
|
||||
export const usersToGroupsConfig = relations(usersToGroupsTable, ({ one }) => ({
|
||||
group: one(groupsTable, {
|
||||
fields: [usersToGroupsTable.groupId],
|
||||
references: [groupsTable.id],
|
||||
}),
|
||||
user: one(usersTable, {
|
||||
fields: [usersToGroupsTable.userId],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const postsTable = sqliteTable('posts', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
content: text('content').notNull(),
|
||||
ownerId: integer('owner_id', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const postsConfig = relations(postsTable, ({ one, many }) => ({
|
||||
author: one(usersTable, {
|
||||
fields: [postsTable.ownerId],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
comments: many(commentsTable),
|
||||
}));
|
||||
|
||||
export const commentsTable = sqliteTable('comments', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
content: text('content').notNull(),
|
||||
creator: integer('creator', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
postId: integer('post_id', { mode: 'number' }).references(() => postsTable.id),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const commentsConfig = relations(commentsTable, ({ one, many }) => ({
|
||||
post: one(postsTable, {
|
||||
fields: [commentsTable.postId],
|
||||
references: [postsTable.id],
|
||||
}),
|
||||
author: one(usersTable, {
|
||||
fields: [commentsTable.creator],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
likes: many(commentLikesTable),
|
||||
}));
|
||||
|
||||
export const commentLikesTable = sqliteTable('comment_likes', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
creator: integer('creator', { mode: 'number' }).references(
|
||||
() => usersTable.id,
|
||||
),
|
||||
commentId: integer('comment_id', { mode: 'number' }).references(
|
||||
() => commentsTable.id,
|
||||
),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull().default(sql`current_timestamp`),
|
||||
});
|
||||
export const commentLikesConfig = relations(commentLikesTable, ({ one }) => ({
|
||||
comment: one(commentsTable, {
|
||||
fields: [commentLikesTable.commentId],
|
||||
references: [commentsTable.id],
|
||||
}),
|
||||
author: one(usersTable, {
|
||||
fields: [commentLikesTable.creator],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = {
|
||||
usersTable,
|
||||
postsTable,
|
||||
commentsTable,
|
||||
usersToGroupsTable,
|
||||
groupsTable,
|
||||
commentLikesConfig,
|
||||
commentsConfig,
|
||||
postsConfig,
|
||||
usersToGroupsConfig,
|
||||
groupsConfig,
|
||||
usersConfig,
|
||||
};
|
||||
|
||||
class ServerSimulator {
|
||||
constructor(private db: BetterSqlite3.Database) {}
|
||||
|
||||
async batch(queries: { sql: string; params: any[]; method: string }[]) {
|
||||
const results: { rows: any }[] = [];
|
||||
for (const query of queries) {
|
||||
const { method, sql, params } = query;
|
||||
|
||||
if (method === 'run') {
|
||||
try {
|
||||
const result = this.db.prepare(sql).run(params);
|
||||
results.push(result as any);
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else if (method === 'all' || method === 'values') {
|
||||
try {
|
||||
const rows = this.db.prepare(sql).raw().all(params);
|
||||
results.push({ rows: rows });
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else if (method === 'get') {
|
||||
try {
|
||||
const row = this.db.prepare(sql).raw().get(params);
|
||||
results.push({ rows: row });
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else {
|
||||
return { error: 'Unknown method value' };
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async query(sql: string, params: any[], method: string) {
|
||||
if (method === 'run') {
|
||||
try {
|
||||
const result = this.db.prepare(sql).run(params);
|
||||
return { data: result as any };
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else if (method === 'all' || method === 'values') {
|
||||
try {
|
||||
const rows = this.db.prepare(sql).raw().all(params);
|
||||
return { data: rows };
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else if (method === 'get') {
|
||||
try {
|
||||
const row = this.db.prepare(sql).raw().get(params);
|
||||
return { data: row };
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else {
|
||||
return { error: 'Unknown method value' };
|
||||
}
|
||||
}
|
||||
|
||||
migrations(queries: string[]) {
|
||||
this.db.exec('BEGIN');
|
||||
try {
|
||||
for (const query of queries) {
|
||||
this.db.exec(query);
|
||||
}
|
||||
this.db.exec('COMMIT');
|
||||
} catch {
|
||||
this.db.exec('ROLLBACK');
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
let db: SqliteRemoteDatabase<typeof schema>;
|
||||
let client: Database.Database;
|
||||
let serverSimulator: ServerSimulator;
|
||||
|
||||
beforeAll(async () => {
|
||||
const dbPath = process.env['SQLITE_DB_PATH'] ?? ':memory:';
|
||||
client = new Database(dbPath);
|
||||
serverSimulator = new ServerSimulator(client);
|
||||
|
||||
db = proxyDrizzle(async (sql, params, method) => {
|
||||
try {
|
||||
// console.log(sql, params, method);
|
||||
const rows = await serverSimulator.query(sql, params, method);
|
||||
|
||||
// console.log('rowsTest', rows);
|
||||
|
||||
if (rows.error !== undefined) {
|
||||
throw new Error(rows.error);
|
||||
}
|
||||
|
||||
return { rows: rows.data };
|
||||
} catch (e: any) {
|
||||
console.error('Error from sqlite proxy server:', e.response.data);
|
||||
throw e;
|
||||
}
|
||||
}, async (queries) => {
|
||||
try {
|
||||
const result = await serverSimulator.batch(queries);
|
||||
|
||||
if ((result as any).error !== undefined) {
|
||||
throw new Error((result as any).error);
|
||||
}
|
||||
|
||||
return result as { rows: any }[];
|
||||
} catch (e: any) {
|
||||
console.error('Error from sqlite proxy server:', e);
|
||||
throw e;
|
||||
}
|
||||
}, { schema });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.run(sql`drop table if exists \`groups\``);
|
||||
await db.run(sql`drop table if exists \`users\``);
|
||||
await db.run(sql`drop table if exists \`users_to_groups\``);
|
||||
await db.run(sql`drop table if exists \`posts\``);
|
||||
await db.run(sql`drop table if exists \`comments\``);
|
||||
await db.run(sql`drop table if exists \`comment_likes\``);
|
||||
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`users\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`name\` text NOT NULL,
|
||||
\`verified\` integer DEFAULT 0 NOT NULL,
|
||||
\`invited_by\` integer
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`groups\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`name\` text NOT NULL,
|
||||
\`description\` text
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`users_to_groups\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`user_id\` integer NOT NULL,
|
||||
\`group_id\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`posts\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`owner_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`comments\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`creator\` integer,
|
||||
\`post_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
await db.run(
|
||||
sql`
|
||||
CREATE TABLE \`comment_likes\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`creator\` integer,
|
||||
\`comment_id\` integer,
|
||||
\`created_at\` integer DEFAULT current_timestamp NOT NULL
|
||||
);
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.run(sql`drop table if exists \`groups\``);
|
||||
await db.run(sql`drop table if exists \`users\``);
|
||||
await db.run(sql`drop table if exists \`users_to_groups\``);
|
||||
await db.run(sql`drop table if exists \`posts\``);
|
||||
await db.run(sql`drop table if exists \`comments\``);
|
||||
await db.run(sql`drop table if exists \`comment_likes\``);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('findMany + findOne api example', async () => {
|
||||
const user = await db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id });
|
||||
const insertRes = await db.insert(usersTable).values({ id: 2, name: 'Dan' });
|
||||
const manyUsers = await db.query.usersTable.findMany({});
|
||||
const oneUser = await db.query.usersTable.findFirst({});
|
||||
|
||||
expectTypeOf(user).toEqualTypeOf<
|
||||
{
|
||||
id: number;
|
||||
}[]
|
||||
>;
|
||||
|
||||
expectTypeOf(insertRes).toEqualTypeOf<SqliteRemoteResult>;
|
||||
|
||||
expectTypeOf(manyUsers).toEqualTypeOf<{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[]>;
|
||||
|
||||
expectTypeOf(oneUser).toEqualTypeOf<
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
} | undefined
|
||||
>;
|
||||
|
||||
expect(user).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(insertRes).toEqual({ rows: { changes: 1, lastInsertRowid: 2 } });
|
||||
|
||||
expect(manyUsers).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(oneUser).toEqual(
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
);
|
||||
});
|
||||
|
||||
test('batch api example', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({
|
||||
id: usersTable.id,
|
||||
invitedBy: usersTable.invitedBy,
|
||||
}),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.select().from(usersTable),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
SqliteRemoteResult,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(3);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
invitedBy: null,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ changes: 1, lastInsertRowid: 2 });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api only relational many
|
||||
test('insert + findMany', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
SqliteRemoteResult,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(3);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ changes: 1, lastInsertRowid: 2 });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api relational many + one
|
||||
test('insert + findMany + findFirst', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.query.usersTable.findFirst({}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
SqliteRemoteResult,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
} | undefined,
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ changes: 1, lastInsertRowid: 2 });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual(
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
);
|
||||
});
|
||||
|
||||
test.skip('insert + db.all + db.get + db.values + db.run', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.run(sql`insert into users (id, name) values (2, 'Dan')`),
|
||||
db.all<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
db.values(sql`select * from users`),
|
||||
db.get<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
SqliteRemoteResult,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
unknown[][],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
},
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(5);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ changes: 1, lastInsertRowid: 2 });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invited_by: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3].map((row) => Array.prototype.slice.call(row))).toEqual([
|
||||
[1, 'John', 0, null],
|
||||
[2, 'Dan', 0, null],
|
||||
]);
|
||||
|
||||
expect(batchResponse[4]).toEqual(
|
||||
{ id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
);
|
||||
});
|
||||
|
||||
// batch api combined rqb + raw call
|
||||
test('insert + findManyWith + db.all', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.all<typeof usersTable.$inferSelect>(sql`select * from users`),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
SqliteRemoteResult,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ changes: 1, lastInsertRowid: 2 });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'John', verified: 0, invitedBy: null },
|
||||
{ id: 2, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual([
|
||||
[1, 'John', 0, null],
|
||||
[2, 'Dan', 0, null],
|
||||
// { id: 1, name: 'John', verified: 0, invited_by: null },
|
||||
// { id: 2, name: 'Dan', verified: 0, invited_by: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api for insert + update + select
|
||||
test('insert + update + select + select partial', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)),
|
||||
db.query.usersTable.findMany({}),
|
||||
db.select().from(usersTable).where(eq(usersTable.id, 1)),
|
||||
db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from(usersTable),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
SqliteRemoteResult,
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
verified: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(5);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ changes: 1, lastInsertRowid: 1 });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual([
|
||||
{ id: 1, name: 'Dan', verified: 0, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[4]).toEqual([
|
||||
{ id: 1, invitedBy: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// batch api for insert + delete + select
|
||||
test('insert + delete + select + select partial', async () => {
|
||||
const batchResponse = await db.batch([
|
||||
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id }),
|
||||
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
|
||||
db.delete(usersTable).where(eq(usersTable.id, 1)).returning({ id: usersTable.id, invitedBy: usersTable.invitedBy }),
|
||||
db.query.usersTable.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
invitedBy: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expectTypeOf(batchResponse).toEqualTypeOf<[
|
||||
{
|
||||
id: number;
|
||||
}[],
|
||||
SqliteRemoteResult,
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
}[],
|
||||
{
|
||||
id: number;
|
||||
invitedBy: number | null;
|
||||
} | undefined,
|
||||
]>();
|
||||
|
||||
expect(batchResponse.length).eq(4);
|
||||
|
||||
expect(batchResponse[0]).toEqual([{
|
||||
id: 1,
|
||||
}]);
|
||||
|
||||
expect(batchResponse[1]).toEqual({ changes: 1, lastInsertRowid: 2 });
|
||||
|
||||
expect(batchResponse[2]).toEqual([
|
||||
{ id: 1, invitedBy: null },
|
||||
]);
|
||||
|
||||
expect(batchResponse[3]).toEqual(
|
||||
{ id: 2, invitedBy: null },
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable drizzle-internal/require-entity-kind */
|
||||
import type BetterSqlite3 from 'better-sqlite3';
|
||||
import Database from 'better-sqlite3';
|
||||
import { Name, sql } from 'drizzle-orm';
|
||||
import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy';
|
||||
import { drizzle as proxyDrizzle } from 'drizzle-orm/sqlite-proxy';
|
||||
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
|
||||
import { skipTests } from '~/common';
|
||||
import { tests, usersTable } from './sqlite-common';
|
||||
import { TestCache, TestGlobalCache, tests as cacheTests } from './sqlite-common-cache';
|
||||
|
||||
class ServerSimulator {
|
||||
constructor(private db: BetterSqlite3.Database) {}
|
||||
|
||||
async query(sql: string, params: any[], method: string) {
|
||||
if (method === 'run') {
|
||||
try {
|
||||
const result = this.db.prepare(sql).run(params);
|
||||
return { data: result as any };
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else if (method === 'all' || method === 'values') {
|
||||
try {
|
||||
const rows = this.db.prepare(sql).raw().all(params);
|
||||
return { data: rows };
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else if (method === 'get') {
|
||||
try {
|
||||
const row = this.db.prepare(sql).raw().get(params);
|
||||
return { data: row };
|
||||
} catch (e: any) {
|
||||
return { error: e.message };
|
||||
}
|
||||
} else {
|
||||
return { error: 'Unknown method value' };
|
||||
}
|
||||
}
|
||||
|
||||
migrations(queries: string[]) {
|
||||
this.db.exec('BEGIN');
|
||||
try {
|
||||
for (const query of queries) {
|
||||
this.db.exec(query);
|
||||
}
|
||||
this.db.exec('COMMIT');
|
||||
} catch {
|
||||
this.db.exec('ROLLBACK');
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
let db: SqliteRemoteDatabase;
|
||||
let dbGlobalCached: SqliteRemoteDatabase;
|
||||
let cachedDb: SqliteRemoteDatabase;
|
||||
let client: Database.Database;
|
||||
let serverSimulator: ServerSimulator;
|
||||
|
||||
beforeAll(async () => {
|
||||
const dbPath = process.env['SQLITE_DB_PATH'] ?? ':memory:';
|
||||
client = new Database(dbPath);
|
||||
serverSimulator = new ServerSimulator(client);
|
||||
|
||||
const callback = async (sql: string, params: any[], method: string) => {
|
||||
try {
|
||||
const rows = await serverSimulator.query(sql, params, method);
|
||||
|
||||
if (rows.error !== undefined) {
|
||||
throw new Error(rows.error);
|
||||
}
|
||||
|
||||
return { rows: rows.data };
|
||||
} catch (e: any) {
|
||||
console.error('Error from sqlite proxy server:', e.response?.data ?? e.message);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
db = proxyDrizzle(callback);
|
||||
cachedDb = proxyDrizzle(callback, { cache: new TestCache() });
|
||||
dbGlobalCached = proxyDrizzle(callback, { cache: new TestGlobalCache() });
|
||||
});
|
||||
|
||||
beforeEach((ctx) => {
|
||||
ctx.sqlite = {
|
||||
db,
|
||||
};
|
||||
ctx.cachedSqlite = {
|
||||
db: cachedDb,
|
||||
dbGlobalCached,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
client?.close();
|
||||
});
|
||||
|
||||
skipTests([
|
||||
// Different driver respond
|
||||
'insert via db.get w/ query builder',
|
||||
'insert via db.run + select via db.get',
|
||||
'insert via db.get',
|
||||
'insert via db.run + select via db.all',
|
||||
]);
|
||||
cacheTests();
|
||||
tests();
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.run(sql`drop table if exists ${usersTable}`);
|
||||
|
||||
await db.run(sql`
|
||||
create table ${usersTable} (
|
||||
id integer primary key,
|
||||
name text not null,
|
||||
verified integer not null default 0,
|
||||
json blob,
|
||||
created_at integer not null default (strftime('%s', 'now'))
|
||||
)
|
||||
`);
|
||||
});
|
||||
|
||||
test('insert via db.get w/ query builder', async () => {
|
||||
const inserted = await db.get<Pick<typeof usersTable.$inferSelect, 'id' | 'name'>>(
|
||||
db.insert(usersTable).values({ name: 'John' }).returning({ id: usersTable.id, name: usersTable.name }),
|
||||
);
|
||||
expect(inserted).toEqual([1, 'John']);
|
||||
});
|
||||
|
||||
test('insert via db.run + select via db.get', async () => {
|
||||
await db.run(sql`insert into ${usersTable} (${new Name(usersTable.name.name)}) values (${'John'})`);
|
||||
|
||||
const result = await db.get<{ id: number; name: string }>(
|
||||
sql`select ${usersTable.id}, ${usersTable.name} from ${usersTable}`,
|
||||
);
|
||||
expect(result).toEqual([1, 'John']);
|
||||
});
|
||||
|
||||
test('insert via db.get', async () => {
|
||||
const inserted = await db.get<{ id: number; name: string }>(
|
||||
sql`insert into ${usersTable} (${new Name(
|
||||
usersTable.name.name,
|
||||
)}) values (${'John'}) returning ${usersTable.id}, ${usersTable.name}`,
|
||||
);
|
||||
expect(inserted).toEqual([1, 'John']);
|
||||
});
|
||||
|
||||
test('insert via db.run + select via db.all', async (ctx) => {
|
||||
const { db } = ctx.sqlite;
|
||||
|
||||
await db.run(sql`insert into ${usersTable} (${new Name(usersTable.name.name)}) values (${'John'})`);
|
||||
|
||||
const result = await db.all<{ id: number; name: string }>(sql`select id, name from "users"`);
|
||||
expect(result).toEqual([[1, 'John']]);
|
||||
});
|
||||
Reference in New Issue
Block a user