From 4762d7818544905e1eb9c85e8d029b9c54fbff85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20Koskim=C3=A4ki?= Date: Wed, 1 Dec 2021 14:20:26 +0200 Subject: [PATCH] Add query logging and the 'call' method --- package.json | 2 +- src/dialect/mysql/mysql-dialect.ts | 9 ++++ src/dialect/mysql/mysql-driver.ts | 7 +-- src/dialect/postgres/postgres-dialect.ts | 12 ++++- src/driver/runtime-driver.ts | 62 ++++++++++++++++++++++-- src/index.ts | 2 +- src/kysely.ts | 41 +++++++++++----- src/parser/join-parser.ts | 24 ++++----- src/query-builder/query-builder.ts | 38 +++++++++++++++ src/util/log.ts | 24 +++++++++ src/util/random-string.ts | 10 ++-- src/util/type-utils.ts | 8 +-- test/src/test-setup.ts | 4 +- test/typings/index.test-d.ts | 11 +++++ 14 files changed, 208 insertions(+), 46 deletions(-) create mode 100644 src/util/log.ts diff --git a/package.json b/package.json index 981e1f2e7..a3913221d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kysely", - "version": "0.9.3", + "version": "0.9.4", "description": "Type safe SQL query builder", "repository": { "type": "git", diff --git a/src/dialect/mysql/mysql-dialect.ts b/src/dialect/mysql/mysql-dialect.ts index fdcff4220..dbf58ec61 100644 --- a/src/dialect/mysql/mysql-dialect.ts +++ b/src/dialect/mysql/mysql-dialect.ts @@ -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 @@ -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 { /** diff --git a/src/dialect/mysql/mysql-driver.ts b/src/dialect/mysql/mysql-driver.ts index f9677d47b..0d014451a 100644 --- a/src/dialect/mysql/mysql-driver.ts +++ b/src/dialect/mysql/mysql-driver.ts @@ -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() diff --git a/src/dialect/postgres/postgres-dialect.ts b/src/dialect/postgres/postgres-dialect.ts index 5353f7a7a..81d2a7732 100644 --- a/src/dialect/postgres/postgres-dialect.ts +++ b/src/dialect/postgres/postgres-dialect.ts @@ -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 @@ -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 diff --git a/src/driver/runtime-driver.ts b/src/driver/runtime-driver.ts index 2ad354d41..fd1c6e13f 100644 --- a/src/driver/runtime-driver.ts +++ b/src/driver/runtime-driver.ts @@ -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' /** @@ -8,11 +10,15 @@ import { Driver, TransactionSettings } from './driver.js' */ export class RuntimeDriver implements Driver { readonly #driver: Driver + readonly #log: Log + #initPromise?: Promise #destroyPromise?: Promise + #connections = new WeakMap() - constructor(driver: Driver) { + constructor(driver: Driver, log: Log) { this.#driver = driver + this.#log = log } async init(): Promise { @@ -28,11 +34,24 @@ export class RuntimeDriver implements Driver { async acquireConnection(): Promise { 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 { - return this.#driver.releaseConnection(connection) + async releaseConnection( + runtimeConnection: DatabaseConnection + ): Promise { + if (runtimeConnection instanceof RuntimeConnection) { + await this.#driver.releaseConnection(runtimeConnection.connection) + } } beginTransaction( @@ -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(compiledQuery: CompiledQuery): Promise> { + const startTime = process.hrtime.bigint() + + try { + return await this.#connection.executeQuery(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 + } +} diff --git a/src/index.ts b/src/index.ts index 39faaf777..866b0eaf8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/kysely.ts b/src/kysely.ts index 407f90d62..8685b18e0 100644 --- a/src/kysely.ts +++ b/src/kysely.ts @@ -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. @@ -71,7 +72,7 @@ export class Kysely extends QueryCreator { 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 @@ -79,8 +80,9 @@ export class Kysely extends QueryCreator { 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( @@ -192,15 +194,20 @@ export class Kysely extends QueryCreator { } /** - * 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') @@ -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 } export class ConnectionBuilder { diff --git a/src/parser/join-parser.ts b/src/parser/join-parser.ts index 6f20f3877..0ca79290c 100644 --- a/src/parser/join-parser.ts +++ b/src/parser/join-parser.ts @@ -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 = - | AnyJoinColumn - | AnyJoinColumnWithTable +export type JoinReferenceExpression = + | AnyJoinColumn + | AnyJoinColumnWithTable -export type JoinCallbackExpression = ( +export type JoinCallbackExpression = ( join: JoinBuilder< - TableExpressionDatabase, - TB | ExtractAliasFromTableExpression + TableExpressionDatabase, + TB | ExtractAliasFromTableExpression > ) => JoinBuilder -type AnyJoinColumn = AnyColumn< - TableExpressionDatabase, - TB | ExtractAliasFromTableExpression +type AnyJoinColumn = AnyColumn< + TableExpressionDatabase, + TB | ExtractAliasFromTableExpression > -type AnyJoinColumnWithTable = AnyColumnWithTable< - TableExpressionDatabase, - TB | ExtractAliasFromTableExpression +type AnyJoinColumnWithTable = AnyColumnWithTable< + TableExpressionDatabase, + TB | ExtractAliasFromTableExpression > export function parseJoin( diff --git a/src/query-builder/query-builder.ts b/src/query-builder/query-builder.ts index 51008b3a1..87399a494 100644 --- a/src/query-builder/query-builder.ts +++ b/src/query-builder/query-builder.ts @@ -2294,6 +2294,44 @@ export class QueryBuilder }) } + /** + * 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(qb: T): T { + * console.log(qb.compile()) + * return qb + * } + * + * db.selectFrom('person') + * .selectAll() + * .call(log) + * .execute() + * ``` + */ + call(func: (qb: this) => T): T { + return func(this) + } + /** * Gives an alias for the query. This method is only useful for sub queries. * diff --git a/src/util/log.ts b/src/util/log.ts new file mode 100644 index 000000000..d6204c871 --- /dev/null +++ b/src/util/log.ts @@ -0,0 +1,24 @@ +import { ArrayItemType } from './type-utils.js' + +export const LOG_LEVELS = ['query'] as const +export type LogLevel = ArrayItemType + +export class Log { + #levels: Readonly> + + constructor(levels: ReadonlyArray) { + 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}`) + } +} diff --git a/src/util/random-string.ts b/src/util/random-string.ts index 090b7f969..a5f2dd33a 100644 --- a/src/util/random-string.ts +++ b/src/util/random-string.ts @@ -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)] } diff --git a/src/util/type-utils.ts b/src/util/type-utils.ts index 3f6906b57..9d99fa2d8 100644 --- a/src/util/type-utils.ts +++ b/src/util/type-utils.ts @@ -178,22 +178,22 @@ export type AliasedRawBuilderFactory = ( export interface InsertResultTypeTag { /** @internal */ - __isInsertResultTypeTag__: true + readonly __isInsertResultTypeTag__: true } export interface DeleteResultTypeTag { /** @internal */ - __isDeleteResultTypeTag__: true + readonly __isDeleteResultTypeTag__: true } export interface UpdateResultTypeTag { /** @internal */ - __isUpdateResultTypeTag__: true + readonly __isUpdateResultTypeTag__: true } export interface GeneratedPlaceholder { /** @internal */ - __isGeneratedPlaceholder__: true + readonly __isGeneratedPlaceholder__: true } export type ManyResultRowType = O extends InsertResultTypeTag diff --git a/test/src/test-setup.ts b/test/src/test-setup.ts index 2884851ad..2597bba65 100644 --- a/test/src/test-setup.ts +++ b/test/src/test-setup.ts @@ -11,7 +11,7 @@ import { PluginTransformQueryArgs, PluginTransformResultArgs, QueryResult, - AnyRow, + UnknownRow, OperationNodeTransformer, PostgresDialect, MysqlDialect, @@ -320,7 +320,7 @@ function createNoopTransformerPlugin(): KyselyPlugin { async transformResult( args: PluginTransformResultArgs - ): Promise> { + ): Promise> { return args.result }, } diff --git a/test/typings/index.test-d.ts b/test/typings/index.test-d.ts index 755559d30..e38c77fc4 100644 --- a/test/typings/index.test-d.ts +++ b/test/typings/index.test-d.ts @@ -627,4 +627,15 @@ async function testUnion(db: Kysely) { ) } +async function testCall(db: Kysely) { + // Table with alias + const [r1] = await db + .selectFrom('pet as p') + .select('p.species') + .call((qb) => qb.select('name')) + .execute() + + expectType<{ species: 'dog' | 'cat' } & { name: string }>(r1) +} + export type Nullable = { [P in keyof T]: T[P] | null }