Skip to content

Commit

Permalink
unit tests pt. 1.
Browse files Browse the repository at this point in the history
  • Loading branch information
igalklebanov committed Dec 28, 2024
1 parent 4095f4a commit a5e3170
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 28 deletions.
10 changes: 5 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
}
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": {
"include": ["src/**/*", "scripts/**/*", "tsconfig.json", "tsup.config.ts"]
"include": ["src/**/*", "scripts/**/*", "test/**/*", "./*.ts", "./*.json"],
"ignore": ["dist/**/*", "strategy/**/*", "config-*.d.ts", "package.json"]
},
"formatter": {
"formatWithErrors": true
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"lint": "biome check",
"check:dts": "attw --pack .",
"check:types": "tsc",
"prepublishOnly": "pnpm build && pnpm check:dts"
"prepublishOnly": "pnpm lint && vitest run && pnpm build && pnpm check:dts",
"test": "vitest"
},
"keywords": [
"kysely",
Expand Down
4 changes: 2 additions & 2 deletions src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,13 @@ export class KyselyReplicationDriver implements Driver {
await connection.rollbackTransaction()
}

#compileErrors(results: PromiseSettledResult<any>[]): string[] {
#compileErrors(results: PromiseSettledResult<unknown>[]): string[] {
return results
.map((result, index) =>
result.status === 'fulfilled'
? null
: `${!index ? 'primary' : `replica-${index - 1}`}: ${result.reason}`,
)
.filter(Boolean) as any
.filter(Boolean) as string[]
}
}
190 changes: 190 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import {
type Dialect,
type Generated,
Kysely,
PostgresAdapter,
PostgresIntrospector,
PostgresQueryCompiler,
type QueryCreator,
type QueryResult,
type SchemaModule,
type SqlBool,
sql,
} from 'kysely'
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
import { KyselyReplicationDialect } from '../src/index.js'
import { RoundRobinReplicaStrategy } from '../src/strategy/round-robin.js'

interface Database {
users: {
id: Generated<number>
email: string
is_verified: SqlBool
}
}

describe('kysely-replication: round-robin', () => {
let db: Kysely<Database>
const executions: string[] = []

beforeAll(() => {
db = new Kysely({
dialect: new KyselyReplicationDialect({
primaryDialect: getDummyDialect('primary', executions),
replicaDialects: new Array(3)
.fill(null)
.map((_, i) =>
getDummyDialect(`replica-${i}`, executions),
) as unknown as readonly [Dialect, ...Dialect[]],
replicaStrategy: new RoundRobinReplicaStrategy({}),
}),
})
})

afterEach(() => {
executions.length = 0 // clear executions
})

it('should use primary dialect for DDL queries', async () => {
const queries = {
alterTable: db.schema
.alterTable('users')
.addColumn('nickname', 'varchar'),
createIndex: db.schema
.createIndex('users_index')
.on('users')
.column('email'),
createSchema: db.schema.createSchema('moshe'),
createTable: db.schema
.createTable('cakes')
.addColumn('id', 'serial', (cb) => cb.primaryKey()),
createType: db.schema
.createType('cake_type')
.asEnum(['chocolate', 'vanilla']),
createView: db.schema
.createView('users_view')
.as(db.selectFrom('users').selectAll()),
dropIndex: db.schema.dropIndex('users_index'),
dropSchema: db.schema.dropSchema('moshe'),
dropTable: db.schema.dropTable('cakes'),
dropType: db.schema.dropType('cake_type'),
dropView: db.schema.dropView('users_view'),
} satisfies {
[K in keyof Omit<SchemaModule, `with${string}`>]: {
execute(): Promise<unknown>
}
}

await Promise.all(Object.values(queries).map((query) => query.execute()))

expect(executions).toEqual(Object.values(queries).map(() => 'primary'))
})

it('should use primary dialect for raw queries', async () => {
await sql`select 1`.execute(db)

expect(executions).toEqual(['primary'])
})

it('should use primary dialect for DML queries that mutate data', async () => {
const queries = {
deleteFrom: db.deleteFrom('users').where('id', '=', 1),
insertInto: db
.insertInto('users')
.values({ email: '[email protected]', is_verified: false }),
mergeInto: db
.mergeInto('users as u1')
.using('users as u2', 'u1.id', 'u2.id')
.whenMatched()
.thenDoNothing(),
replaceInto: db
.replaceInto('users')
.values({ email: '[email protected]', is_verified: false }),
updateTable: db
.updateTable('users')
.set('is_verified', true)
.where('id', '=', 1),
with: db
.with('insert', (qb) =>
qb
.insertInto('users')
.values({ email: '[email protected]', is_verified: false })
.returning('id'),
)
.selectFrom('users')
.innerJoin('insert', 'insert.id', 'users.id')
.selectAll(),
} satisfies {
[K in
| keyof Omit<
QueryCreator<Database>,
`select${string}` | `with${string}`
>
| 'with']: {
execute(): Promise<unknown>
}
}

await Promise.all(Object.values(queries).map((query) => query.execute()))

expect(executions).toEqual(Object.values(queries).map(() => 'primary'))
})

it('should use replica dialects for DML queries that do not mutate data', async () => {
const queries = {
selectFrom: db.selectFrom('users').selectAll(),
selectNoFrom: db
.selectNoFrom((eb) => eb.selectFrom('users').selectAll().as('u'))
.selectAll(),
with: db
.with('u1', (qb) => qb.selectFrom('users').selectAll())
.selectFrom('u1')
.selectAll(),
} satisfies {
[K in keyof Pick<
QueryCreator<Database>,
'selectFrom' | 'selectNoFrom' | 'with'
>]: { execute(): Promise<unknown> }
}

await Promise.all([
Object.values(queries).map((query) => query.execute()),
db.selectFrom('users').selectAll().where('id', '=', 1).execute(),
db.selectFrom('users').selectAll().where('id', '=', 2).execute(),
])

expect(executions).toEqual([
'replica-0',
'replica-1',
'replica-2',
'replica-0',
'replica-1',
])
})
})

function getDummyDialect(name: string, executions: string[]): Dialect {
return {
createAdapter: () => new PostgresAdapter(),
createDriver: () => ({
acquireConnection: () => {
executions.push(name)
return Promise.resolve({
executeQuery: () =>
Promise.resolve({ rows: [] } satisfies QueryResult<unknown>),
streamQuery: () => {
throw new Error('Not implemented')
},
})
},
beginTransaction: () => Promise.resolve(),
commitTransaction: () => Promise.resolve(),
destroy: () => Promise.resolve(),
init: () => Promise.resolve(),
releaseConnection: () => Promise.resolve(),
rollbackTransaction: () => Promise.resolve(),
}),
createIntrospector: (db) => new PostgresIntrospector(db),
createQueryCompiler: () => new PostgresQueryCompiler(),
}
}
4 changes: 4 additions & 0 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.base.json",
"include": ["./*.ts"]
}
18 changes: 18 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "./node_modules/@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "preserve",
"moduleResolution": "bundler",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
20 changes: 1 addition & 19 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
{
"extends": "./node_modules/@tsconfig/node22/tsconfig.json",
"compilerOptions": {
// overrides
"module": "preserve",
"moduleResolution": "bundler",

// custom
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"extends": "./tsconfig.base.json",
"include": ["src"]
}
12 changes: 12 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
allowOnly: false,
globalSetup: ['./vitest.setup.ts'],
typecheck: {
enabled: true,
ignoreSourceErrors: true,
},
},
})
1 change: 1 addition & 0 deletions vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => {}

0 comments on commit a5e3170

Please sign in to comment.