Skip to content

Commit

Permalink
Add query logging and the 'call' method
Browse files Browse the repository at this point in the history
  • Loading branch information
koskimas committed Dec 1, 2021
1 parent ccc4681 commit 4762d78
Show file tree
Hide file tree
Showing 14 changed files with 208 additions and 46 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kysely",
"version": "0.9.3",
"version": "0.9.4",
"description": "Type safe SQL query builder",
"repository": {
"type": "git",
Expand Down
9 changes: 9 additions & 0 deletions src/dialect/mysql/mysql-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { DialectAdapter } from '../dialect-adapter.js'
import { MysqlAdapter } from './mysql-adapter.js'
import { DatabaseConnection } from '../../driver/database-connection.js'

/**
* MySQL dialect that uses the [mysql2](https://github.com/sidorares/node-mysql2#readme) library.
*
* The {@link MysqlDialectConfig | configuration} passed to the constructor
* is given as-is to the mysql2 library's [createPool](https://github.com/sidorares/node-mysql2#using-connection-pools)
* method.
*/
export class MysqlDialect implements Dialect {
readonly #config: MysqlDialectConfig

Expand Down Expand Up @@ -38,6 +45,8 @@ export class MysqlDialect implements Dialect {
* Config for the mysql dialect.
*
* This interface is equal to `mysql2` library's pool config.
*
* https://github.com/sidorares/node-mysql2#using-connection-pools
*/
export interface MysqlDialectConfig {
/**
Expand Down
7 changes: 1 addition & 6 deletions src/dialect/mysql/mysql-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@ import {

import { Driver, TransactionSettings } from '../../driver/driver.js'
import { CompiledQuery } from '../../query-compiler/compiled-query.js'
import {
isFunction,
isNumber,
isObject,
isString,
} from '../../util/object-utils.js'
import { isFunction, isNumber, isObject } from '../../util/object-utils.js'
import { MysqlDialectConfig } from './mysql-dialect.js'

const PRIVATE_RELEASE_METHOD = Symbol()
Expand Down
12 changes: 11 additions & 1 deletion src/dialect/postgres/postgres-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import { DialectAdapter } from '../dialect-adapter.js'
import { PostgresAdapter } from './postgres-adapter.js'
import { DatabaseConnection } from '../../driver/database-connection.js'

/**
* PostgreSQL dialect that uses the [pg](https://node-postgres.com/) library.
*
* The {@link PostgresDialectConfig | configuration} passed to the constructor
* is given as-is to the pg library's [Pool](https://node-postgres.com/api/pool)
* constructor. See the following two links for more documentation:
*
* https://node-postgres.com/api/pool
* https://node-postgres.com/api/client
*/
export class PostgresDialect implements Dialect {
readonly #config: PostgresDialectConfig

Expand Down Expand Up @@ -40,7 +50,7 @@ export class PostgresDialect implements Dialect {
/**
* Config for the postgres dialect.
*
* This interface is equal to `pg` library's pool config:
* This interface is equal to `pg` library's `Pool` config:
*
* https://node-postgres.com/api/pool
* https://node-postgres.com/api/client
Expand Down
62 changes: 57 additions & 5 deletions src/driver/runtime-driver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { DatabaseConnection } from './database-connection.js'
import { CompiledQuery } from '../query-compiler/compiled-query.js'
import { Log } from '../util/log.js'
import { DatabaseConnection, QueryResult } from './database-connection.js'
import { Driver, TransactionSettings } from './driver.js'

/**
Expand All @@ -8,11 +10,15 @@ import { Driver, TransactionSettings } from './driver.js'
*/
export class RuntimeDriver implements Driver {
readonly #driver: Driver
readonly #log: Log

#initPromise?: Promise<void>
#destroyPromise?: Promise<void>
#connections = new WeakMap<DatabaseConnection, RuntimeConnection>()

constructor(driver: Driver) {
constructor(driver: Driver, log: Log) {
this.#driver = driver
this.#log = log
}

async init(): Promise<void> {
Expand All @@ -28,11 +34,24 @@ export class RuntimeDriver implements Driver {

async acquireConnection(): Promise<DatabaseConnection> {
await this.init()
return this.#driver.acquireConnection()

const connection = await this.#driver.acquireConnection()
let runtimeConnection = this.#connections.get(connection)

if (!runtimeConnection) {
runtimeConnection = new RuntimeConnection(connection, this.#log)
this.#connections.set(connection, runtimeConnection)
}

return runtimeConnection
}

releaseConnection(connection: DatabaseConnection): Promise<void> {
return this.#driver.releaseConnection(connection)
async releaseConnection(
runtimeConnection: DatabaseConnection
): Promise<void> {
if (runtimeConnection instanceof RuntimeConnection) {
await this.#driver.releaseConnection(runtimeConnection.connection)
}
}

beginTransaction(
Expand Down Expand Up @@ -67,3 +86,36 @@ export class RuntimeDriver implements Driver {
await this.#destroyPromise
}
}

class RuntimeConnection implements DatabaseConnection {
readonly #connection: DatabaseConnection
readonly #log: Log

get connection(): DatabaseConnection {
return this.#connection
}

constructor(connection: DatabaseConnection, log: Log) {
this.#connection = connection
this.#log = log
}

async executeQuery<R>(compiledQuery: CompiledQuery): Promise<QueryResult<R>> {
const startTime = process.hrtime.bigint()

try {
return await this.#connection.executeQuery<R>(compiledQuery)
} finally {
this.#log.query((log) => {
log(compiledQuery.sql)
log(`duration: ${this.#calculateDurationMillis(startTime)}ms`)
})
}
}

#calculateDurationMillis(startTime: bigint): number {
const endTime = process.hrtime.bigint()
const durationTensMillis = Number((endTime - startTime) / 1_00_000n)
return durationTensMillis / 10
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export * from './operation-node/order-by-node.js'
export * from './introspection/database-introspector.js'

export * from './util/compilable.js'
export { AnyColumn, UnknownRow as AnyRow } from './util/type-utils.js'
export { AnyColumn, UnknownRow, AnyQueryBuilder } from './util/type-utils.js'

export {
SelectExpression,
Expand Down
41 changes: 30 additions & 11 deletions src/kysely.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { preventAwait } from './util/prevent-await.js'
import { DefaultParseContext, ParseContext } from './parser/parse-context.js'
import { FunctionBuilder } from './query-builder/function-builder.js'
import { Log, LogLevel } from './util/log.js'

/**
* The main Kysely class.
Expand Down Expand Up @@ -71,16 +72,17 @@ export class Kysely<DB> extends QueryCreator<DB> {
constructor(args: KyselyConfig | KyselyProps) {
if (isKyselyProps(args)) {
super({ executor: args.executor, parseContext: args.parseContext })
this.#props = freeze(args)
this.#props = freeze({ ...args })
} else {
const dialect = args.dialect

const driver = dialect.createDriver()
const compiler = dialect.createQueryCompiler()
const adapter = dialect.createAdapter()

const log = new Log(args.log ?? [])
const parseContext = new DefaultParseContext(adapter)
const runtimeDriver = new RuntimeDriver(driver)
const runtimeDriver = new RuntimeDriver(driver, log)

const connectionProvider = new DefaultConnectionProvider(runtimeDriver)
const executor = new DefaultQueryExecutor(
Expand Down Expand Up @@ -192,15 +194,20 @@ export class Kysely<DB> extends QueryCreator<DB> {
}

/**
* Starts a transaction. If the callback throws the transaction is rolled back,
* otherwise it's committed.
* Creates a {@link TransactionBuilder} that can be used to run queries inside a transaction.
*
* @example
* In the example below if either query fails or `someFunction` throws, both inserts
* will be rolled back. Otherwise the transaction will be committed by the time the
* `transaction` function returns the output value. The output value of the
* `transaction` method is the value returned from the callback.
* The returned {@link TransactionBuilder} can be used to configure the transaction. The
* {@link TransactionBuilder.execute} method can then be called to run the transaction.
* {@link TransactionBuilder.execute} takes a function that is run inside the
* transaction. If the function throws, the transaction is rolled back. Otherwise
* the transaction is committed.
*
* The callback function passed to the {@link TransactionBuilder.execute | execute}
* method gets the transaction object as its only argument. The transaction is
* of type {@link Transaction} which inherits {@link Kysely}. Any query
* started through the transaction object is executed inside the transaction.
*
* @example
* ```ts
* const catto = await db.transaction().execute(async (trx) => {
* const jennifer = await trx.insertInto('person')
Expand Down Expand Up @@ -335,8 +342,20 @@ export function isKyselyProps(obj: unknown): obj is KyselyProps {
}

export interface KyselyConfig {
dialect: Dialect
plugins?: KyselyPlugin[]
readonly dialect: Dialect
readonly plugins?: KyselyPlugin[]

/**
* A list of log levels to log.
*
* Currently there's only one level: `query` and it's logged using
* `console.log`. This will be expanded based on user request later.
*
* Log levels:
*
* - query: Log each query's SQL and duration.
*/
readonly log?: ReadonlyArray<LogLevel>
}

export class ConnectionBuilder<DB> {
Expand Down
24 changes: 12 additions & 12 deletions src/parser/join-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@ import { parseReferenceFilter } from './filter-parser.js'
import { JoinBuilder } from '../query-builder/join-builder.js'
import { ParseContext } from './parse-context.js'

export type JoinReferenceExpression<DB, TB extends keyof DB, F> =
| AnyJoinColumn<DB, TB, F>
| AnyJoinColumnWithTable<DB, TB, F>
export type JoinReferenceExpression<DB, TB extends keyof DB, TE> =
| AnyJoinColumn<DB, TB, TE>
| AnyJoinColumnWithTable<DB, TB, TE>

export type JoinCallbackExpression<DB, TB extends keyof DB, F> = (
export type JoinCallbackExpression<DB, TB extends keyof DB, TE> = (
join: JoinBuilder<
TableExpressionDatabase<DB, F>,
TB | ExtractAliasFromTableExpression<DB, F>
TableExpressionDatabase<DB, TE>,
TB | ExtractAliasFromTableExpression<DB, TE>
>
) => JoinBuilder<any, any>

type AnyJoinColumn<DB, TB extends keyof DB, F> = AnyColumn<
TableExpressionDatabase<DB, F>,
TB | ExtractAliasFromTableExpression<DB, F>
type AnyJoinColumn<DB, TB extends keyof DB, TE> = AnyColumn<
TableExpressionDatabase<DB, TE>,
TB | ExtractAliasFromTableExpression<DB, TE>
>

type AnyJoinColumnWithTable<DB, TB extends keyof DB, F> = AnyColumnWithTable<
TableExpressionDatabase<DB, F>,
TB | ExtractAliasFromTableExpression<DB, F>
type AnyJoinColumnWithTable<DB, TB extends keyof DB, TE> = AnyColumnWithTable<
TableExpressionDatabase<DB, TE>,
TB | ExtractAliasFromTableExpression<DB, TE>
>

export function parseJoin(
Expand Down
38 changes: 38 additions & 0 deletions src/query-builder/query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2294,6 +2294,44 @@ export class QueryBuilder<DB, TB extends keyof DB, O = {}>
})
}

/**
* Simply calls the given method passing `this` as the only argument.
*
* This method can be useful when adding optional method calls:
*
* @example
* ```ts
* db.selectFrom('person')
* .selectAll()
* .call((qb) => {
* if (something) {
* return qb.where('something', '=', something)
* } else {
* return qb.where('somethingElse', '=', somethingElse)
* }
* })
* .execute()
* ```
*
* The next example uses a helper funtion `log` to log a query:
*
* @example
* ```ts
* function log<T extends AnyQueryBuilder>(qb: T): T {
* console.log(qb.compile())
* return qb
* }
*
* db.selectFrom('person')
* .selectAll()
* .call(log)
* .execute()
* ```
*/
call<T>(func: (qb: this) => T): T {
return func(this)
}

/**
* Gives an alias for the query. This method is only useful for sub queries.
*
Expand Down
24 changes: 24 additions & 0 deletions src/util/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ArrayItemType } from './type-utils.js'

export const LOG_LEVELS = ['query'] as const
export type LogLevel = ArrayItemType<typeof LOG_LEVELS>

export class Log {
#levels: Readonly<Record<LogLevel, boolean>>

constructor(levels: ReadonlyArray<LogLevel>) {
this.#levels = {
query: levels.includes('query'),
}
}

query(callback: (log: (message: string) => void) => void) {
if (this.#levels.query) {
callback(this.#query)
}
}

#query(message: string) {
console.log(`kysely:query: ${message}`)
}
}
10 changes: 7 additions & 3 deletions src/util/random-string.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'

export function randomString(length: number) {
let chars: string[] = new Array(length)
let chars = ''

for (let i = 0; i < length; ++i) {
chars[i] = CHARS[Math.floor(Math.random() * CHARS.length)]
chars += randomChar()
}

return chars.join('')
return chars
}

function randomChar() {
return CHARS[Math.floor(Math.random() * CHARS.length)]
}
Loading

0 comments on commit 4762d78

Please sign in to comment.