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<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> {
@@ -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(
@@ -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
+  }
+}
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<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
 
@@ -79,8 +80,9 @@ export class Kysely<DB> extends QueryCreator<DB> {
       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<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')
@@ -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> {
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<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(
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<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.
    *
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<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}`)
+  }
+}
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<DB, TB extends keyof DB> = (
 
 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> = 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<QueryResult<AnyRow>> {
+    ): Promise<QueryResult<UnknownRow>> {
       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<Database>) {
   )
 }
 
+async function testCall(db: Kysely<Database>) {
+  // 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<T> = { [P in keyof T]: T[P] | null }