Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions deno.check.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Generated,
GeneratedAlways,
Insertable,
Json,
Kysely,
Selectable,
SqlBool,
Expand All @@ -13,6 +14,7 @@ export interface Database {
audit: AuditTable
person: PersonTable
person_backup: PersonTable
person_metadata: PersonMetadataTable
pet: PetTable
toy: ToyTable
wine: WineTable
Expand Down Expand Up @@ -47,6 +49,14 @@ interface PersonTable {
marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null
}

interface PersonMetadataTable {
id: Generated<number>
personId: number
experience: Json<{ title: string; company: string }[]>
preferences: Json<{ locale: string; timezone: string }> | null
profile: Json<{ email_verified: boolean }> | null
}

interface PetTable {
id: Generated<number>
created_at: GeneratedAlways<Date>
Expand Down
1 change: 1 addition & 0 deletions deno.check.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"types": ["./deno.check.d.ts"]
},
"imports": {
"@electric-sql/pglite": "npm:@electric-sql/pglite",
"better-sqlite3": "npm:better-sqlite3",
"kysely": "./dist/esm",
"kysely/helpers/mssql": "./dist/esm/helpers/mssql.js",
Expand Down
10 changes: 10 additions & 0 deletions site/docs/getting-started/Summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const postgresqlCodeSnippet = ` await db.schema.createTable('person')
.addColumn('created_at', 'timestamp', (cb) =>
cb.notNull().defaultTo(sql\`now()\`)
)
.addColumn('metadata', 'jsonb', (cb) => cb.notNull())
.execute()`

const dialectSpecificCodeSnippets: Record<Dialect, string> = {
Expand All @@ -24,6 +25,7 @@ const dialectSpecificCodeSnippets: Record<Dialect, string> = {
.addColumn('created_at', 'timestamp', (cb) =>
cb.notNull().defaultTo(sql\`now()\`)
)
.addColumn('metadata', 'json', (cb) => cb.notNull())
.execute()`,
// TODO: Update line 42's IDENTITY once identity(1,1) is added to core.
mssql: ` await db.schema.createTable('person')
Expand All @@ -34,6 +36,7 @@ const dialectSpecificCodeSnippets: Record<Dialect, string> = {
.addColumn('created_at', 'datetime', (cb) =>
cb.notNull().defaultTo(sql\`GETDATE()\`)
)
.addColumn('metadata', sql\`nvarchar(max)\`, (cb) => cb.notNull())
.execute()`,
sqlite: ` await db.schema.createTable('person')
.addColumn('id', 'integer', (cb) => cb.primaryKey().autoIncrement().notNull())
Expand All @@ -43,6 +46,7 @@ const dialectSpecificCodeSnippets: Record<Dialect, string> = {
.addColumn('created_at', 'timestamp', (cb) =>
cb.notNull().defaultTo(sql\`current_timestamp\`)
)
.addColumn('metadata', 'text', (cb) => cb.notNull())
.execute()`,
pglite: postgresqlCodeSnippet,
}
Expand Down Expand Up @@ -109,6 +113,12 @@ ${dialectSpecificCodeSnippet}
first_name: 'Jennifer',
last_name: 'Aniston',
gender: 'woman',
metadata: sql.jval({
login_at: new Date().toISOString(),
ip: null,
agent: null,
plan: 'free',
}),
})
})

Expand Down
12 changes: 5 additions & 7 deletions site/docs/getting-started/_types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ColumnType,
Generated,
Insertable,
JSONColumnType,
Json,
Selectable,
Updateable,
} from 'kysely'
Expand Down Expand Up @@ -45,12 +45,10 @@ export interface PersonTable {
// can never be updated:
created_at: ColumnType<Date, string | undefined, never>

// You can specify JSON columns using the `JSONColumnType` wrapper.
// It is a shorthand for `ColumnType<T, string, string>`, where T
// is the type of the JSON object/array retrieved from the database,
// and the insert and update types are always `string` since you're
// always stringifying insert/update values.
metadata: JSONColumnType<{
// You can specify JSON columns using the `Json` wrapper.
// When inserting/updating values of such columns, you're required to wrap the
// values with `eb.jval` or `sql.jval`.
metadata: Json<{
login_at: string
ip: string | null
agent: string | null
Expand Down
4 changes: 2 additions & 2 deletions src/dialect/pglite/pglite-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ export class PGliteAdapter extends PostgresAdapter {
return false
}

async acquireMigrationLock(): Promise<void> {
override async acquireMigrationLock(): Promise<void> {
// PGlite only has one connection that's reserved by the migration system
// for the whole time between acquireMigrationLock and releaseMigrationLock.
// We don't need to do anything here.
}

async releaseMigrationLock(): Promise<void> {
override async releaseMigrationLock(): Promise<void> {
// PGlite only has one connection that's reserved by the migration system
// for the whole time between acquireMigrationLock and releaseMigrationLock.
// We don't need to do anything here.
Expand Down
2 changes: 1 addition & 1 deletion src/dialect/pglite/pglite-dialect-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface PGliteDialectConfig {
*
* https://pglite.dev/docs/api#main-constructor
*/
pglite: PGlite | (() => Promise<PGlite>)
pglite: PGlite | (() => PGlite | Promise<PGlite>)
}

/**
Expand Down
45 changes: 44 additions & 1 deletion src/expression/expression-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import {
ValTuple5,
} from '../parser/tuple-parser.js'
import { TupleNode } from '../operation-node/tuple-node.js'
import { Selectable } from '../util/column-type.js'
import { Selectable, Serialized } from '../util/column-type.js'
import { JSONPathNode } from '../operation-node/json-path-node.js'
import { KyselyTypeError } from '../util/type-error.js'
import {
Expand All @@ -78,6 +78,7 @@ import {
} from '../parser/data-type-parser.js'
import { CastNode } from '../operation-node/cast-node.js'
import { SelectFrom } from '../parser/select-from-parser.js'
import { ValueNode } from '../operation-node/value-node.js'

export interface ExpressionBuilder<DB, TB extends keyof DB> {
/**
Expand Down Expand Up @@ -590,6 +591,42 @@ export interface ExpressionBuilder<DB, TB extends keyof DB> {
value: VE,
): ExpressionWrapper<DB, TB, ExtractTypeFromValueExpression<VE>>

/**
* Returns a value expression that will be serialized before being passed to the database.
*
* This can be used to pass in an object/array value when inserting/updating a
* value to a column defined with `Json`.
*
* Default serializer function is `JSON.stringify`.
*
* ### Example
*
* ```ts
* import { Json } from 'kysely'
*
* interface Database {
* person_metadata: {
* experience: Json<{ title: string; company: string }[]>
* preferences: Json<{ locale: string; timezone: string }>
* profile: Json<{ email_verified: boolean }>
* }
* }
*
* const result = await db
* .insertInto('person_metadata')
* .values(({ jval }) => ({
* personId: 123,
* experience: jval([{ title: 'Software Engineer', company: 'Google' }]), // ✔️
* // preferences: jval({ locale: 'en' }), // ❌ missing `timezone`
* // profile: JSON.stringify({ email_verified: true }), // ❌ doesn't match `Serialized<{ email_verified }>`
* }))
* .execute()
* ```
*/
jval<O extends object | null>(
obj: O,
): ExpressionWrapper<DB, TB, Serialized<O>>

/**
* Creates a tuple expression.
*
Expand Down Expand Up @@ -1233,6 +1270,12 @@ export function createExpressionBuilder<DB, TB extends keyof DB>(
return new ExpressionWrapper(parseValueExpression(value))
},

jval<O extends object | null>(
value: O,
): ExpressionWrapper<DB, TB, Serialized<O>> {
return new ExpressionWrapper(ValueNode.createSerialized(value))
},

refTuple(
...values: ReadonlyArray<ReferenceExpression<any, any>>
): ExpressionWrapper<DB, TB, any> {
Expand Down
9 changes: 9 additions & 0 deletions src/operation-node/value-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface ValueNode extends OperationNode {
readonly kind: 'ValueNode'
readonly value: unknown
readonly immediate?: boolean
readonly serialized?: boolean
}

/**
Expand All @@ -29,4 +30,12 @@ export const ValueNode = freeze({
immediate: true,
})
},

createSerialized(value: unknown): ValueNode {
return freeze({
kind: 'ValueNode',
value,
serialized: true,
})
},
})
10 changes: 10 additions & 0 deletions src/query-compiler/default-query-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ export class DefaultQueryCompiler
protected override visitValue(node: ValueNode): void {
if (node.immediate) {
this.appendImmediateValue(node.value)
} else if (node.serialized) {
this.appendSerializedValue(node.value)
} else {
this.appendValue(node.value)
}
Expand Down Expand Up @@ -1757,6 +1759,14 @@ export class DefaultQueryCompiler
this.append(this.getCurrentParameterPlaceholder())
}

protected appendSerializedValue(parameter: unknown): void {
if (parameter === null) {
this.appendValue(null)
} else {
this.appendValue(JSON.stringify(parameter))
}
}

protected getLeftIdentifierWrapper(): string {
return '"'
}
Expand Down
24 changes: 24 additions & 0 deletions src/raw-builder/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ValueNode } from '../operation-node/value-node.js'
import { parseStringReference } from '../parser/reference-parser.js'
import { parseTable } from '../parser/table-parser.js'
import { parseValueExpression } from '../parser/value-parser.js'
import { Serialized } from '../util/column-type.js'
import { createQueryId } from '../util/query-id.js'
import { RawBuilder, createRawBuilder } from './raw-builder.js'

Expand Down Expand Up @@ -137,6 +138,22 @@ export interface Sql {
*/
val<V>(value: V): RawBuilder<V>

/**
* `sql.jval(value)` is a shortcut for:
*
* ```ts
* import { Serialized, sql } from 'kysely'
*
* const serializerFn = JSON.stringify
* const obj = { hello: 'world!' }
*
* sql<Serialized<typeof obj>>`${serializerFn(obj)}`
* ```
*
* Default serializer function is `JSON.stringify`.
*/
jval<O extends object | null>(value: O): RawBuilder<Serialized<O>>

/**
* @deprecated Use {@link Sql.val} instead.
*/
Expand Down Expand Up @@ -417,6 +434,13 @@ export const sql: Sql = Object.assign(
})
},

jval<O extends object | null>(value: O): RawBuilder<Serialized<O>> {
return createRawBuilder({
queryId: createQueryId(),
rawNode: RawNode.createWithChild(ValueNode.createSerialized(value)),
})
},

value<V>(value: V): RawBuilder<V> {
return this.val(value)
},
Expand Down
28 changes: 28 additions & 0 deletions src/util/column-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,37 @@ export type Generated<S> = ColumnType<S, S | undefined, S>
*/
export type GeneratedAlways<S> = ColumnType<S, never, never>

/**
* A shortcut for defining type-safe JSON columns. Inserts/updates require passing
* values that are wrapped with `eb.jval` or `sql.jval` instead of `JSON.stringify`.
*/
export type Json<
SelectType extends object | null,
InsertType extends Serialized<SelectType> | Extract<null, SelectType> =
| Serialized<SelectType>
| Extract<null, SelectType>,
UpdateType extends Serialized<SelectType> | Extract<null, SelectType> =
| Serialized<SelectType>
| Extract<null, SelectType>,
> = ColumnType<SelectType, InsertType, UpdateType>

/**
* A symbol that is used to brand serialized objects/arrays.
* @internal
*/
declare const SerializedBrand: unique symbol

/**
* A type that is used to brand serialized objects/arrays.
*/
export type Serialized<O extends object | null> = O & {
readonly [SerializedBrand]: '⚠️ When you insert into or update columns of type `Json` (or similar), you should wrap your JSON value with `eb.jval` or `sql.jval`, instead of `JSON.stringify`. ⚠️'
}

/**
* A shortcut for defining JSON columns, which are by default inserted/updated
* as stringified JSON strings.
* @deprecated Use {@link Json} instead.
*/
export type JSONColumnType<
SelectType extends object | null,
Expand Down
Loading