Skip to content

Commit 01b823d

Browse files
committed
introduce Serialized<O>
introduce ValueNode.serialized. introduce eb.valSerialized. introduce sql.valSerialized. fix json-traversal test suite. fix null handling @ compiler. rename to `valJson`. add instructions in errors. typings test inserts. call the new type `Json` instead, to not introduce a breaking change. add missing json column @ Getting Started. add `appendSerializedValue`. Renames `valJson` to `jval` for JSON value wrapping Renames the `valJson` method to `jval` for wrapping JSON values when inserting or updating columns. This change promotes brevity and consistency throughout the codebase. The name change affects the expression builder, SQL raw builder, and documentation. fix jsdocs check.
1 parent 9e6e9d3 commit 01b823d

File tree

14 files changed

+286
-38
lines changed

14 files changed

+286
-38
lines changed

deno.check.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
Generated,
44
GeneratedAlways,
55
Insertable,
6+
Json,
67
Kysely,
78
Selectable,
89
SqlBool,
@@ -13,6 +14,7 @@ export interface Database {
1314
audit: AuditTable
1415
person: PersonTable
1516
person_backup: PersonTable
17+
person_metadata: PersonMetadataTable
1618
pet: PetTable
1719
toy: ToyTable
1820
wine: WineTable
@@ -47,6 +49,14 @@ interface PersonTable {
4749
marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null
4850
}
4951

52+
interface PersonMetadataTable {
53+
id: Generated<number>
54+
personId: number
55+
experience: Json<{ title: string; company: string }[]>
56+
preferences: Json<{ locale: string; timezone: string }> | null
57+
profile: Json<{ email_verified: boolean }> | null
58+
}
59+
5060
interface PetTable {
5161
id: Generated<number>
5262
created_at: GeneratedAlways<Date>

deno.check.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"types": ["./deno.check.d.ts"]
55
},
66
"imports": {
7+
"@electric-sql/pglite": "npm:@electric-sql/pglite",
78
"better-sqlite3": "npm:better-sqlite3",
89
"kysely": "./dist/esm",
910
"kysely/helpers/mssql": "./dist/esm/helpers/mssql.js",

site/docs/getting-started/Summary.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const postgresqlCodeSnippet = ` await db.schema.createTable('person')
1212
.addColumn('created_at', 'timestamp', (cb) =>
1313
cb.notNull().defaultTo(sql\`now()\`)
1414
)
15+
.addColumn('metadata', 'jsonb', (cb) => cb.notNull())
1516
.execute()`
1617

1718
const dialectSpecificCodeSnippets: Record<Dialect, string> = {
@@ -24,6 +25,7 @@ const dialectSpecificCodeSnippets: Record<Dialect, string> = {
2425
.addColumn('created_at', 'timestamp', (cb) =>
2526
cb.notNull().defaultTo(sql\`now()\`)
2627
)
28+
.addColumn('metadata', 'json', (cb) => cb.notNull())
2729
.execute()`,
2830
// TODO: Update line 42's IDENTITY once identity(1,1) is added to core.
2931
mssql: ` await db.schema.createTable('person')
@@ -34,6 +36,7 @@ const dialectSpecificCodeSnippets: Record<Dialect, string> = {
3436
.addColumn('created_at', 'datetime', (cb) =>
3537
cb.notNull().defaultTo(sql\`GETDATE()\`)
3638
)
39+
.addColumn('metadata', sql\`nvarchar(max)\`, (cb) => cb.notNull())
3740
.execute()`,
3841
sqlite: ` await db.schema.createTable('person')
3942
.addColumn('id', 'integer', (cb) => cb.primaryKey().autoIncrement().notNull())
@@ -43,6 +46,7 @@ const dialectSpecificCodeSnippets: Record<Dialect, string> = {
4346
.addColumn('created_at', 'timestamp', (cb) =>
4447
cb.notNull().defaultTo(sql\`current_timestamp\`)
4548
)
49+
.addColumn('metadata', 'text', (cb) => cb.notNull())
4650
.execute()`,
4751
pglite: postgresqlCodeSnippet,
4852
}
@@ -109,6 +113,12 @@ ${dialectSpecificCodeSnippet}
109113
first_name: 'Jennifer',
110114
last_name: 'Aniston',
111115
gender: 'woman',
116+
metadata: sql.jval({
117+
login_at: new Date().toISOString(),
118+
ip: null,
119+
agent: null,
120+
plan: 'free',
121+
}),
112122
})
113123
})
114124

site/docs/getting-started/_types.mdx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
ColumnType,
1111
Generated,
1212
Insertable,
13-
JSONColumnType,
13+
Json,
1414
Selectable,
1515
Updateable,
1616
} from 'kysely'
@@ -45,12 +45,10 @@ export interface PersonTable {
4545
// can never be updated:
4646
created_at: ColumnType<Date, string | undefined, never>
4747

48-
// You can specify JSON columns using the `JSONColumnType` wrapper.
49-
// It is a shorthand for `ColumnType<T, string, string>`, where T
50-
// is the type of the JSON object/array retrieved from the database,
51-
// and the insert and update types are always `string` since you're
52-
// always stringifying insert/update values.
53-
metadata: JSONColumnType<{
48+
// You can specify JSON columns using the `Json` wrapper.
49+
// When inserting/updating values of such columns, you're required to wrap the
50+
// values with `eb.jval` or `sql.jval`.
51+
metadata: Json<{
5452
login_at: string
5553
ip: string | null
5654
agent: string | null

src/dialect/pglite/pglite-adapter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ export class PGliteAdapter extends PostgresAdapter {
55
return false
66
}
77

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

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

src/dialect/pglite/pglite-dialect-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface PGliteDialectConfig {
1919
*
2020
* https://pglite.dev/docs/api#main-constructor
2121
*/
22-
pglite: PGlite | (() => Promise<PGlite>)
22+
pglite: PGlite | (() => PGlite | Promise<PGlite>)
2323
}
2424

2525
/**

src/expression/expression-builder.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import {
6969
ValTuple5,
7070
} from '../parser/tuple-parser.js'
7171
import { TupleNode } from '../operation-node/tuple-node.js'
72-
import { Selectable } from '../util/column-type.js'
72+
import { Selectable, Serialized } from '../util/column-type.js'
7373
import { JSONPathNode } from '../operation-node/json-path-node.js'
7474
import { KyselyTypeError } from '../util/type-error.js'
7575
import {
@@ -78,6 +78,7 @@ import {
7878
} from '../parser/data-type-parser.js'
7979
import { CastNode } from '../operation-node/cast-node.js'
8080
import { SelectFrom } from '../parser/select-from-parser.js'
81+
import { ValueNode } from '../operation-node/value-node.js'
8182

8283
export interface ExpressionBuilder<DB, TB extends keyof DB> {
8384
/**
@@ -590,6 +591,42 @@ export interface ExpressionBuilder<DB, TB extends keyof DB> {
590591
value: VE,
591592
): ExpressionWrapper<DB, TB, ExtractTypeFromValueExpression<VE>>
592593

594+
/**
595+
* Returns a value expression that will be serialized before being passed to the database.
596+
*
597+
* This can be used to pass in an object/array value when inserting/updating a
598+
* value to a column defined with `Json`.
599+
*
600+
* Default serializer function is `JSON.stringify`.
601+
*
602+
* ### Example
603+
*
604+
* ```ts
605+
* import { Json } from 'kysely'
606+
*
607+
* interface Database {
608+
* person_metadata: {
609+
* experience: Json<{ title: string; company: string }[]>
610+
* preferences: Json<{ locale: string; timezone: string }>
611+
* profile: Json<{ email_verified: boolean }>
612+
* }
613+
* }
614+
*
615+
* const result = await db
616+
* .insertInto('person_metadata')
617+
* .values(({ jval }) => ({
618+
* personId: 123,
619+
* experience: jval([{ title: 'Software Engineer', company: 'Google' }]), // ✔️
620+
* // preferences: jval({ locale: 'en' }), // ❌ missing `timezone`
621+
* // profile: JSON.stringify({ email_verified: true }), // ❌ doesn't match `Serialized<{ email_verified }>`
622+
* }))
623+
* .execute()
624+
* ```
625+
*/
626+
jval<O extends object | null>(
627+
obj: O,
628+
): ExpressionWrapper<DB, TB, Serialized<O>>
629+
593630
/**
594631
* Creates a tuple expression.
595632
*
@@ -1233,6 +1270,14 @@ export function createExpressionBuilder<DB, TB extends keyof DB>(
12331270
return new ExpressionWrapper(parseValueExpression(value))
12341271
},
12351272

1273+
jval<O extends object | null>(
1274+
value: O,
1275+
): ExpressionWrapper<DB, TB, Serialized<O>> {
1276+
return new ExpressionWrapper(
1277+
ValueNode.create(value, { serialized: true }),
1278+
)
1279+
},
1280+
12361281
refTuple(
12371282
...values: ReadonlyArray<ReferenceExpression<any, any>>
12381283
): ExpressionWrapper<DB, TB, any> {

src/operation-node/value-node.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface ValueNode extends OperationNode {
55
readonly kind: 'ValueNode'
66
readonly value: unknown
77
readonly immediate?: boolean
8+
readonly serialized?: boolean
89
}
910

1011
/**
@@ -15,9 +16,10 @@ export const ValueNode = freeze({
1516
return node.kind === 'ValueNode'
1617
},
1718

18-
create(value: unknown): ValueNode {
19+
create(value: unknown, props?: { serialized?: boolean }): ValueNode {
1920
return freeze({
2021
kind: 'ValueNode',
22+
...props,
2123
value,
2224
})
2325
},

src/query-compiler/default-query-compiler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,8 @@ export class DefaultQueryCompiler
519519
protected override visitValue(node: ValueNode): void {
520520
if (node.immediate) {
521521
this.appendImmediateValue(node.value)
522+
} else if (node.serialized) {
523+
this.appendSerializedValue(node.value)
522524
} else {
523525
this.appendValue(node.value)
524526
}
@@ -1757,6 +1759,14 @@ export class DefaultQueryCompiler
17571759
this.append(this.getCurrentParameterPlaceholder())
17581760
}
17591761

1762+
protected appendSerializedValue(parameter: unknown): void {
1763+
if (parameter === null) {
1764+
this.appendValue(null)
1765+
} else {
1766+
this.appendValue(JSON.stringify(parameter))
1767+
}
1768+
}
1769+
17601770
protected getLeftIdentifierWrapper(): string {
17611771
return '"'
17621772
}

src/raw-builder/sql.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ValueNode } from '../operation-node/value-node.js'
66
import { parseStringReference } from '../parser/reference-parser.js'
77
import { parseTable } from '../parser/table-parser.js'
88
import { parseValueExpression } from '../parser/value-parser.js'
9+
import { Serialized } from '../util/column-type.js'
910
import { createQueryId } from '../util/query-id.js'
1011
import { RawBuilder, createRawBuilder } from './raw-builder.js'
1112

@@ -137,6 +138,22 @@ export interface Sql {
137138
*/
138139
val<V>(value: V): RawBuilder<V>
139140

141+
/**
142+
* `sql.jval(value)` is a shortcut for:
143+
*
144+
* ```ts
145+
* import { Serialized, sql } from 'kysely'
146+
*
147+
* const serializerFn = JSON.stringify
148+
* const obj = { hello: 'world!' }
149+
*
150+
* sql<Serialized<typeof obj>>`${serializerFn(obj)}`
151+
* ```
152+
*
153+
* Default serializer function is `JSON.stringify`.
154+
*/
155+
jval<O extends object | null>(value: O): RawBuilder<Serialized<O>>
156+
140157
/**
141158
* @deprecated Use {@link Sql.val} instead.
142159
*/
@@ -417,6 +434,15 @@ export const sql: Sql = Object.assign(
417434
})
418435
},
419436

437+
jval<O extends object | null>(value: O): RawBuilder<Serialized<O>> {
438+
return createRawBuilder({
439+
queryId: createQueryId(),
440+
rawNode: RawNode.createWithChild(
441+
ValueNode.create(value, { serialized: true }),
442+
),
443+
})
444+
},
445+
420446
value<V>(value: V): RawBuilder<V> {
421447
return this.val(value)
422448
},

0 commit comments

Comments
 (0)