Skip to content

Commit

Permalink
feat: add support for libsql and cleanup clients to dialects mapping
Browse files Browse the repository at this point in the history
Deprecates: connection.dialectName in favor of connection.clientName
  • Loading branch information
thetutlage committed Jun 18, 2024
1 parent 1b32dca commit 95f11d1
Show file tree
Hide file tree
Showing 20 changed files with 196 additions and 100 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"scripts": {
"pretest": "npm run lint",
"test:better_sqlite": "cross-env DB=better_sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js",
"test:libsql": "cross-env DB=libsql node --enable-source-maps --loader=ts-node/esm ./bin/test.js",
"test:sqlite": "cross-env DB=sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js",
"test:mysql": "cross-env DB=mysql node --enable-source-maps --loader=ts-node/esm ./bin/test.js",
"test:mysql_legacy": "cross-env DB=mysql_legacy node --enable-source-maps --loader=ts-node/esm ./bin/test.js",
Expand Down Expand Up @@ -68,6 +69,7 @@
"@japa/assert": "^3.0.0",
"@japa/file-system": "^2.3.0",
"@japa/runner": "^3.1.4",
"@libsql/sqlite3": "^0.3.1",
"@swc/core": "^1.6.1",
"@types/chance": "^1.1.6",
"@types/luxon": "^3.4.2",
Expand Down
14 changes: 14 additions & 0 deletions src/clients/libsql.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const Sqlite3Client = require('knex/lib/dialects/sqlite3')

class LibSQLClient extends Sqlite3Client {
_driver() {
return require('@libsql/sqlite3')
}
}

Object.assign(LibSQLClient.prototype, {
dialect: 'libsql',
driverName: 'libsql',
})

module.exports = LibSQLClient
35 changes: 31 additions & 4 deletions src/connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import { patchKnex } from 'knex-dynamic-connection'
import type { Logger } from '@adonisjs/core/logger'
// @ts-expect-error
import { resolveClientNameWithAliases } from 'knex/lib/util/helpers.js'
import { ConnectionConfig, ConnectionContract } from '../types/database.js'

import { Logger as ConnectionLogger } from './logger.js'
import * as errors from '../errors.js'
import { clientsNames } from '../dialects/index.js'
// @ts-expect-error
import LibSQLClient from '../clients/libsql.cjs'
import { Logger as ConnectionLogger } from './logger.js'
import type { ConnectionConfig, ConnectionContract } from '../types/database.js'

/**
* Connection class manages a given database connection. Internally it uses
Expand All @@ -38,10 +41,17 @@ export class Connection extends EventEmitter implements ConnectionContract {
readClient?: Knex

/**
* Connection dialect name
* Connection dialect name.
* @deprecated
* @see clientName
*/
dialectName: ConnectionContract['dialectName']

/**
* Connection client name.
*/
clientName: ConnectionContract['dialectName']

/**
* A boolean to know if connection operates on read/write
* replicas
Expand All @@ -66,12 +76,18 @@ export class Connection extends EventEmitter implements ConnectionContract {
) {
super()
this.validateConfig()
this.dialectName = resolveClientNameWithAliases(this.config.client)
this.clientName = resolveClientNameWithAliases(this.config.client)
this.dialectName = this.clientName

this.hasReadWriteReplicas = !!(
this.config.replicas &&
this.config.replicas.read &&
this.config.replicas.write
)

if (!clientsNames.includes(this.clientName)) {
throw new errors.E_UNSUPPORTED_CLIENT([this.clientName])
}
}

/**
Expand Down Expand Up @@ -133,6 +149,17 @@ export class Connection extends EventEmitter implements ConnectionContract {
*/
private getWriteConfig(): Knex.Config {
if (!this.config.replicas) {
/**
* Replacing string based libsql client with the
* actual implementation
*/
if (this.config.client === 'libsql') {
return {
...this.config,
client: LibSQLClient,
}
}

return this.config
}

Expand Down
2 changes: 1 addition & 1 deletion src/dialects/base_sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import type { DialectContract, QueryClientContract, SharedConfigNode } from '../types/database.js'

export abstract class BaseSqliteDialect implements DialectContract {
abstract readonly name: 'sqlite3' | 'better-sqlite3'
abstract readonly name: 'sqlite3' | 'better-sqlite3' | 'libsql'
readonly supportsAdvisoryLocks = false
readonly supportsViews = true
readonly supportsTypes = false
Expand Down
17 changes: 14 additions & 3 deletions src/dialects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@
import { PgDialect } from './pg.js'
import { MysqlDialect } from './mysql.js'
import { MssqlDialect } from './mssql.js'
import { LibSQLDialect } from './libsql.js'
import { SqliteDialect } from './sqlite.js'
import { OracleDialect } from './oracle.js'
import { RedshiftDialect } from './red_shift.js'
import { BetterSqliteDialect } from './better_sqlite.js'
import { DialectContract, QueryClientContract, SharedConfigNode } from '../types/database.js'
import {
DialectContract,
SharedConfigNode,
QueryClientContract,
ConnectionContract,
} from '../types/database.js'

export const dialects: {
[key: string]: {
export const clientsToDialectsMapping: {
[K in ConnectionContract['clientName']]: {
new (client: QueryClientContract, config: SharedConfigNode): DialectContract
}
} = {
Expand All @@ -28,5 +34,10 @@ export const dialects: {
'postgres': PgDialect,
'redshift': RedshiftDialect,
'sqlite3': SqliteDialect,
'libsql': LibSQLDialect,
'better-sqlite3': BetterSqliteDialect,
}

export const clientsNames = Object.keys(
clientsToDialectsMapping
) as ConnectionContract['clientName'][]
15 changes: 15 additions & 0 deletions src/dialects/libsql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { DialectContract } from '../types/database.js'
import { BaseSqliteDialect } from './base_sqlite.js'

export class LibSQLDialect extends BaseSqliteDialect implements DialectContract {
readonly name = 'libsql'
}
9 changes: 9 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,12 @@ export const E_UNDEFINED_RELATIONSHIP = createError(
)

export const E_RUNTIME_EXCEPTION = createError('%s', 'E_RUNTIME_EXCEPTION', 500)

/**
* The client is not supported by Lucid
*/
export const E_UNSUPPORTED_CLIENT = createError<[string]>(
'Unsupported client "%s"',
'E_UNSUPPORTED_CLIENT',
500
)
9 changes: 6 additions & 3 deletions src/query_client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
TransactionClientContract,
} from '../types/database.js'

import { dialects } from '../dialects/index.js'
import { TransactionClient } from '../transaction_client/index.js'
import { RawBuilder } from '../database/static_builder/raw.js'
import { clientsToDialectsMapping } from '../dialects/index.js'
import { TransactionClient } from '../transaction_client/index.js'
import { RawQueryBuilder } from '../database/query_builder/raw.js'
import { InsertQueryBuilder } from '../database/query_builder/insert.js'
import { ReferenceBuilder } from '../database/static_builder/reference.js'
Expand Down Expand Up @@ -72,7 +72,10 @@ export class QueryClient implements QueryClientContract {
) {
this.debug = !!this.connection.config.debug
this.connectionName = this.connection.name
this.dialect = new dialects[this.connection.dialectName](this, this.connection.config)
this.dialect = new clientsToDialectsMapping[this.connection.clientName](
this,
this.connection.config
)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/query_runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class QueryRunner {
* Is query dialect using sqlite database or not
*/
private isUsingSqlite() {
return this.client.dialect.name === 'sqlite3'
return ['sqlite3', 'better-sqlite3', 'libsql'].includes(this.client.dialect.name)
}

/**
Expand Down
37 changes: 37 additions & 0 deletions src/types/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface DialectContract {
| 'postgres'
| 'redshift'
| 'sqlite3'
| 'libsql'
| 'better-sqlite3'
readonly dateTimeFormat: string

Expand Down Expand Up @@ -371,6 +372,24 @@ export type SqliteConfig = SharedConfigNode & {
replicas?: never
}

/**
* The LibSQL specific config options are taken directly from the
* driver. https://github.com/mapbox/node-sqlite3/wiki/API#new-sqlite3databasefilename-mode-callback
*
* LibSQL dialect is a drop-in replacement for SQLite and hence the config
* options are same
*/
export type LibSQLConfig = SharedConfigNode & {
client: 'libsql'
connection: {
filename: string
flags?: string[]
debug?: boolean
mode?: any
}
replicas?: never
}

/**
* The MYSQL specific config options are taken directly from the
* driver. https://www.npmjs.com/package/mysql#connection-options
Expand Down Expand Up @@ -543,6 +562,7 @@ export type MssqlConfig = SharedConfigNode & {
*/
export type ConnectionConfig =
| SqliteConfig
| LibSQLConfig
| MysqlConfig
| PostgreConfig
| OracleConfig
Expand Down Expand Up @@ -638,6 +658,10 @@ export interface ConnectionContract extends EventEmitter {
client?: Knex
readClient?: Knex

/**
* @deprecated
* @see clientName
*/
readonly dialectName:
| 'mssql'
| 'mysql'
Expand All @@ -646,6 +670,19 @@ export interface ConnectionContract extends EventEmitter {
| 'postgres'
| 'redshift'
| 'sqlite3'
| 'libsql'
| 'better-sqlite3'

readonly clientName:
| 'mssql'
| 'mysql'
| 'mysql2'
| 'oracledb'
| 'postgres'
| 'redshift'
| 'sqlite3'
| 'libsql'
| 'better-sqlite3'

/**
* Property to find if explicit read/write is enabled
Expand Down
34 changes: 31 additions & 3 deletions test-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import { InsertQueryBuilder } from '../src/database/query_builder/insert.js'
import { LucidRow, LucidModel, AdapterContract } from '../src/types/model.js'
import { DatabaseQueryBuilder } from '../src/database/query_builder/database.js'

// @ts-expect-error
import LibSQLClient from '../src/clients/libsql.cjs'

dotenv.config()
export const APP_ROOT = new URL('./tmp', import.meta.url)
export const SQLITE_BASE_PATH = fileURLToPath(APP_ROOT)
Expand All @@ -66,6 +69,15 @@ export function getConfig(): ConnectionConfig {
useNullAsDefault: true,
debug: !!process.env.DEBUG,
}
case 'libsql':
return {
client: 'libsql',
connection: {
filename: `file:${join(SQLITE_BASE_PATH, 'libsql.db')}`,
},
useNullAsDefault: true,
debug: !!process.env.DEBUG,
}
case 'better_sqlite':
return {
client: 'better-sqlite3',
Expand Down Expand Up @@ -138,11 +150,27 @@ export function getConfig(): ConnectionConfig {
}
}

/**
* Returns an instance of knex for testing
*/
export function getKnex(config: knex.Knex.Config): knex.Knex {
return knex.knex(
Object.assign(
{},
{
...config,
client: config.client === 'libsql' ? LibSQLClient : config.client,
},
{ debug: false }
)
)
}

/**
* Does base setup by creating databases
*/
export async function setup(destroyDb: boolean = true) {
const db = knex.knex(Object.assign({}, getConfig(), { debug: false }))
const db = getKnex(Object.assign({}, getConfig(), { debug: false }))

const hasUsersTable = await db.schema.hasTable('users')
if (!hasUsersTable) {
Expand Down Expand Up @@ -292,7 +320,7 @@ export async function setup(destroyDb: boolean = true) {
* Does cleanup removes database
*/
export async function cleanup(customTables?: string[]) {
const db = knex.knex(Object.assign({}, getConfig(), { debug: false }))
const db = getKnex(Object.assign({}, getConfig(), { debug: false }))

if (customTables) {
for (let table of customTables) {
Expand Down Expand Up @@ -324,7 +352,7 @@ export async function cleanup(customTables?: string[]) {
* Reset database tables
*/
export async function resetTables() {
const db = knex.knex(Object.assign({}, getConfig(), { debug: false }))
const db = getKnex(Object.assign({}, getConfig(), { debug: false }))
await db.table('users').truncate()
await db.table('uuid_users').truncate()
await db.table('follows').truncate()
Expand Down
Binary file added test-helpers/tmp/libsql.db
Binary file not shown.
24 changes: 1 addition & 23 deletions test/connection/connection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { MysqlConfig } from '../../src/types/database.js'
import { Connection } from '../../src/connection/index.js'
import { setup, cleanup, getConfig, resetTables, logger } from '../../test-helpers/index.js'

if (process.env.DB !== 'sqlite') {
if (!['sqlite', 'better_sqlite', 'libsql'].includes(process.env.DB!)) {
test.group('Connection | config', (group) => {
group.setup(async () => {
await setup()
Expand Down Expand Up @@ -122,28 +122,6 @@ test.group('Connection | setup', (group) => {

await connection.disconnect()
}).waitForDone()

test('raise error when unable to make connection', ({ assert }, done) => {
assert.plan(2)

const connection = new Connection(
'primary',
Object.assign({}, getConfig(), { client: null }),
logger
)

connection.on('error', ({ message }) => {
try {
assert.equal(message, "knex: Required configuration option 'client' is missing.")
done()
} catch (error) {
done(error)
}
})

const fn = () => connection.connect()
assert.throws(fn, /knex: Required configuration option/)
}).waitForDone()
})

if (process.env.DB === 'mysql') {
Expand Down
Loading

0 comments on commit 95f11d1

Please sign in to comment.