diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4a0a17600..3afe0e966 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -208,10 +208,17 @@ jobs:
- name: Run build with newer TypeScript
run: npm run build
- - name: Install older TypeScript
- run: npm i -D typescript@${{ matrix.typescript-version }} tsd@${{ fromJson('{ "^4.6":"0.20.0", "^4.7":"0.22.0", "^4.8":"0.24.1", "^4.9":"0.27.0", "^5.0":"0.28.1", "^5.2":"0.29.0", "^5.3":"0.30.7", "^5.4":"0.31.2" }')[matrix.typescript-version] }}
+ - run: |
+ echo "TS_VERSION=${{ matrix.typescript-version }}" >> $GITHUB_ENV
+ echo "TSD_VERSION=${{ fromJson('{ "^4.6":"0.20.0", "^4.7":"0.22.0", "^4.8":"0.24.1", "^4.9":"0.27.0", "^5.0":"0.28.1", "^5.2":"0.29.0", "^5.3":"0.30.7", "^5.4":"0.31.2" }')[env.TS_VERSION] }} >> $GITHUB_ENV"
- - name: Run tests with older TypeScript
+ - name: Install Typescript (${{ env.TS_VERSION }}) and TSD (${{ env.TSD_VERSION }})
+ run: npm i -D typescript@${{ env.TS_VERSION }} tsd@${{ env.TSD_VERSION }}
+
+ - name: Exclude non-backward compatible tests
+ run: npx tsx ./scripts/exclude-test-files-for-backwards-compat.mts
+
+ - name: Run tests with older TypeScript version
run: npm run test:typings && npm run test:node:build
jsdocs:
diff --git a/.npmignore b/.npmignore
index c92c384f7..d9e1ad890 100644
--- a/.npmignore
+++ b/.npmignore
@@ -16,4 +16,4 @@ package-lock.json
tsconfig-base.json
tsconfig-cjs.json
tsconfig.json
-
+CONTRIBUTING.md
diff --git a/outdated-typescript.d.ts b/outdated-typescript.d.ts
new file mode 100644
index 000000000..ec280583e
--- /dev/null
+++ b/outdated-typescript.d.ts
@@ -0,0 +1,5 @@
+import type { KyselyTypeError } from './dist/cjs/util/type-error'
+
+declare const Kysely: KyselyTypeError<'The installed TypeScript version is outdated and cannot guarantee type-safety with Kysely. Please upgrade to version 4.6 or newer.'>
+declare const RawBuilder: KyselyTypeError<'The installed TypeScript version is outdated and cannot guarantee type-safety with Kysely. Please upgrade to version 4.6 or newer.'>
+declare const sql: KyselyTypeError<'The installed TypeScript version is outdated and cannot guarantee type-safety with Kysely. Please upgrade to version 4.6 or newer.'>
diff --git a/package-lock.json b/package-lock.json
index dc3a67a66..2e0f9838f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"@types/node": "^22.5.0",
"@types/pg": "^8.11.6",
"@types/pg-cursor": "^2.7.2",
+ "@types/semver": "^7.5.8",
"@types/sinon": "^17.0.2",
"@types/tedious": "^4.0.9",
"better-sqlite3": "^11.2.1",
@@ -28,19 +29,22 @@
"lodash": "^4.17.21",
"mocha": "^10.7.3",
"mysql2": "^3.11.0",
+ "pathe": "^1.1.2",
"pg": "^8.12.0",
"pg-cursor": "^2.11.0",
"pkg-pr-new": "^0.0.30",
"playwright": "^1.46.1",
"prettier": "^3.3.3",
+ "semver": "^7.6.3",
"sinon": "^19.0.2",
"tarn": "^3.0.2",
"tedious": "^19.0.0",
"tsd": "^0.31.1",
+ "tsx": "^4.19.1",
"typescript": "^5.6.3"
},
"engines": {
- "node": ">=14.0.0"
+ "node": ">=18.0.0"
}
},
"node_modules/@andrewbranch/untar.js": {
@@ -1531,6 +1535,12 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
+ "node_modules/@types/semver": {
+ "version": "7.5.8",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
+ "dev": true
+ },
"node_modules/@types/sinon": {
"version": "17.0.3",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz",
@@ -2535,6 +2545,7 @@
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -2579,6 +2590,18 @@
"node": "*"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
+ "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
+ "dev": true,
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -4054,6 +4077,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -4320,9 +4344,9 @@
}
},
"node_modules/react-is": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
- "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true
},
"node_modules/read-pkg": {
@@ -4519,6 +4543,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -4579,13 +4612,10 @@
"dev": true
},
"node_modules/semver": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
- "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
"bin": {
"semver": "bin/semver.js"
},
@@ -4593,18 +4623,6 @@
"node": ">=10"
}
},
- "node_modules/semver/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
@@ -5146,9 +5164,9 @@
}
},
"node_modules/tsd": {
- "version": "0.31.1",
- "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.1.tgz",
- "integrity": "sha512-sSL84A0SFwx2xGMWrxlGaarKFSQszWjJS2vgNDDLwatytzg2aq6ShlwHsBYxRNmjzXISODwMva5ZOdAg/4AoOA==",
+ "version": "0.31.2",
+ "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.2.tgz",
+ "integrity": "sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==",
"dev": true,
"dependencies": {
"@tsd/typescript": "~5.4.3",
@@ -5172,6 +5190,473 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
},
+ "node_modules/tsx": {
+ "version": "4.19.1",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz",
+ "integrity": "sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "~0.23.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
+ "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
+ "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
+ "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
+ "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
+ "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
+ "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
+ "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
+ "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
+ "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
+ "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
+ "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
+ "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
+ "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
+ "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
+ "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
+ "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
+ "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
+ "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
+ "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
+ "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
+ "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
+ "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
+ "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
+ "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
+ "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.23.1",
+ "@esbuild/android-arm": "0.23.1",
+ "@esbuild/android-arm64": "0.23.1",
+ "@esbuild/android-x64": "0.23.1",
+ "@esbuild/darwin-arm64": "0.23.1",
+ "@esbuild/darwin-x64": "0.23.1",
+ "@esbuild/freebsd-arm64": "0.23.1",
+ "@esbuild/freebsd-x64": "0.23.1",
+ "@esbuild/linux-arm": "0.23.1",
+ "@esbuild/linux-arm64": "0.23.1",
+ "@esbuild/linux-ia32": "0.23.1",
+ "@esbuild/linux-loong64": "0.23.1",
+ "@esbuild/linux-mips64el": "0.23.1",
+ "@esbuild/linux-ppc64": "0.23.1",
+ "@esbuild/linux-riscv64": "0.23.1",
+ "@esbuild/linux-s390x": "0.23.1",
+ "@esbuild/linux-x64": "0.23.1",
+ "@esbuild/netbsd-x64": "0.23.1",
+ "@esbuild/openbsd-arm64": "0.23.1",
+ "@esbuild/openbsd-x64": "0.23.1",
+ "@esbuild/sunos-x64": "0.23.1",
+ "@esbuild/win32-arm64": "0.23.1",
+ "@esbuild/win32-ia32": "0.23.1",
+ "@esbuild/win32-x64": "0.23.1"
+ }
+ },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
diff --git a/package.json b/package.json
index 493d394d1..60fd04100 100644
--- a/package.json
+++ b/package.json
@@ -7,10 +7,17 @@
"url": "git://github.com/kysely-org/kysely.git"
},
"engines": {
- "node": ">=14.0.0"
+ "node": ">=18.0.0"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
+ "typesVersions": {
+ "<4.6": {
+ "*": [
+ "outdated-typescript.d.ts"
+ ]
+ }
+ },
"exports": {
".": {
"import": "./dist/esm/index.js",
@@ -54,6 +61,7 @@
"test:esbuild": "esbuild --bundle --platform=node --external:pg-native dist/esm/index.js --outfile=/dev/null",
"test:exports": "attw --pack . && node scripts/check-exports.js",
"test:jsdocs": "deno check --doc-only --no-lock --unstable-sloppy-imports --config=\"deno.check.json\" ./src",
+ "test:outdatedts": "npm run build && cd test/outdated-ts && npm ci && npm test",
"prettier": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
"build": "npm run clean && (npm run build:esm & npm run build:cjs) && npm run script:module-fixup && npm run script:copy-interface-doc",
"build:esm": "tsc -p tsconfig.json && npm run script:add-deno-type-references",
@@ -82,6 +90,7 @@
"@types/node": "^22.5.0",
"@types/pg": "^8.11.6",
"@types/pg-cursor": "^2.7.2",
+ "@types/semver": "^7.5.8",
"@types/sinon": "^17.0.2",
"@types/tedious": "^4.0.9",
"better-sqlite3": "^11.2.1",
@@ -92,15 +101,18 @@
"lodash": "^4.17.21",
"mocha": "^10.7.3",
"mysql2": "^3.11.0",
+ "pathe": "^1.1.2",
"pg": "^8.12.0",
"pg-cursor": "^2.11.0",
"pkg-pr-new": "^0.0.30",
"playwright": "^1.46.1",
"prettier": "^3.3.3",
+ "semver": "^7.6.3",
"sinon": "^19.0.2",
"tarn": "^3.0.2",
"tedious": "^19.0.0",
"tsd": "^0.31.1",
+ "tsx": "^4.19.1",
"typescript": "^5.6.3"
}
}
diff --git a/scripts/copy-interface-documentation.js b/scripts/copy-interface-documentation.js
index 56a8a0df4..735886870 100644
--- a/scripts/copy-interface-documentation.js
+++ b/scripts/copy-interface-documentation.js
@@ -22,7 +22,7 @@ const OBJECT_REGEXES = [
/^(?:export )?declare (?:abstract )?class (\w+)/,
/^(?:export )?interface (\w+)/,
]
-const GENERIC_ARGUMENTS_REGEX = /<[\w"'`,{}=| ]+>/g
+const GENERIC_ARGUMENTS_REGEX = /<[\w"'`,{}=|\[\] ]+>/g
const JSDOC_START_REGEX = /^\s+\/\*\*/
const JSDOC_END_REGEX = /^\s+\*\//
@@ -123,7 +123,7 @@ function parseObjects(file) {
function parseImplements(line) {
if (!line.endsWith('{')) {
console.warn(
- `skipping object declaration "${line}". Expected it to end with "{"'`
+ `skipping object declaration "${line}". Expected it to end with "{"'`,
)
return []
}
@@ -225,7 +225,7 @@ function findDocProperty(files, object, propertyName) {
}
const interfaceProperty = interfaceObject.properties.find(
- (it) => it.name === propertyName
+ (it) => it.name === propertyName,
)
if (interfaceProperty?.doc) {
diff --git a/scripts/exclude-test-files-for-backwards-compat.mts b/scripts/exclude-test-files-for-backwards-compat.mts
new file mode 100644
index 000000000..9378dc76d
--- /dev/null
+++ b/scripts/exclude-test-files-for-backwards-compat.mts
@@ -0,0 +1,22 @@
+import { writeFile } from 'node:fs/promises'
+import { dirname, resolve } from 'pathe'
+import { lt } from 'semver'
+import { devDependencies } from '../package.json'
+
+const typescriptVersion = devDependencies.typescript.replace('^', '')
+const testTsConfigRelativePath = '../test/node/tsconfig.json'
+
+if (lt(typescriptVersion, '5.0.0')) {
+ const tsconfig = await import('../test/node/tsconfig.json')
+
+ await writeFile(
+ resolve(
+ dirname(new URL(import.meta.url).pathname),
+ testTsConfigRelativePath,
+ ),
+ JSON.stringify({
+ ...tsconfig,
+ exclude: [...(tsconfig.exclude || []), 'src/async-dispose.test.ts'],
+ }),
+ )
+}
diff --git a/site/docs/examples/update/0010-single-row.js b/site/docs/examples/update/0010-single-row.js
index e82a23a65..b30a98077 100644
--- a/site/docs/examples/update/0010-single-row.js
+++ b/site/docs/examples/update/0010-single-row.js
@@ -5,6 +5,4 @@ export const singleRow = `const result = await db
last_name: 'Aniston'
})
.where('id', '=', '1')
- .executeTakeFirst()
-
-console.log(result.numUpdatedRows)`
\ No newline at end of file
+ .executeTakeFirst()`
\ No newline at end of file
diff --git a/site/docs/examples/update/0020-complex-values.js b/site/docs/examples/update/0020-complex-values.js
index 3b88fee1e..caad0ebb6 100644
--- a/site/docs/examples/update/0020-complex-values.js
+++ b/site/docs/examples/update/0020-complex-values.js
@@ -6,6 +6,4 @@ export const complexValues = `const result = await db
last_name: 'updated',
}))
.where('id', '=', '1')
- .executeTakeFirst()
-
-console.log(result.numUpdatedRows)`
\ No newline at end of file
+ .executeTakeFirst()`
\ No newline at end of file
diff --git a/site/docs/examples/update/0030-my-sql-joins.js b/site/docs/examples/update/0030-my-sql-joins.js
new file mode 100644
index 000000000..400b23c44
--- /dev/null
+++ b/site/docs/examples/update/0030-my-sql-joins.js
@@ -0,0 +1,7 @@
+export const mySqlJoins = `const result = await db
+ .updateTable(['person', 'pet'])
+ .set('person.first_name', 'Updated person')
+ .set('pet.name', 'Updated doggo')
+ .whereRef('person.id', '=', 'pet.owner_id')
+ .where('person.id', '=', '1')
+ .executeTakeFirst()`
\ No newline at end of file
diff --git a/site/docs/examples/update/0030-my-sql-joins.mdx b/site/docs/examples/update/0030-my-sql-joins.mdx
new file mode 100644
index 000000000..82c4db139
--- /dev/null
+++ b/site/docs/examples/update/0030-my-sql-joins.mdx
@@ -0,0 +1,40 @@
+---
+title: 'MySQL joins'
+---
+
+# MySQL joins
+
+MySQL allows you to join tables directly to the "main" table and update
+rows of all joined tables. This is possible by passing all tables to the
+`updateTable` method as a list and adding the `ON` conditions as `WHERE`
+statements. You can then use the `set(column, value)` variant to update
+columns using table qualified names.
+
+The `UpdateQueryBuilder` also has `innerJoin` etc. join methods, but those
+can only be used as part of a PostgreSQL `update set from join` query.
+Due to type complexity issues, we unfortunately can't make the same
+methods work in both cases.
+
+import {
+ Playground,
+ exampleSetup,
+} from '../../../src/components/Playground'
+
+import {
+ mySqlJoins
+} from './0030-my-sql-joins'
+
+
+
+:::info[More examples]
+The API documentation is packed with examples. The API docs are hosted [here](https://kysely-org.github.io/kysely-apidoc/),
+but you can access the same documentation by hovering over functions/methods/classes in your IDE. The examples are always
+just one hover away!
+
+For example, check out these sections:
+ - [set method](https://kysely-org.github.io/kysely-apidoc/classes/UpdateQueryBuilder.html#set)
+ - [returning method](https://kysely-org.github.io/kysely-apidoc/classes/UpdateQueryBuilder.html#returning)
+ - [updateTable method](https://kysely-org.github.io/kysely-apidoc/classes/Kysely.html#updateTable)
+:::
diff --git a/site/docs/getting-started/Summary.tsx b/site/docs/getting-started/Summary.tsx
index 6fed7f893..555c0869a 100644
--- a/site/docs/getting-started/Summary.tsx
+++ b/site/docs/getting-started/Summary.tsx
@@ -2,11 +2,7 @@ import Admonition from '@theme/Admonition'
import CodeBlock from '@theme/CodeBlock'
import Link from '@docusaurus/Link'
import { IUseADifferentDatabase } from './IUseADifferentDatabase'
-import {
- PRETTY_DIALECT_NAMES,
- type Dialect,
- type PropsWithDialect,
-} from './shared'
+import { type Dialect, type PropsWithDialect } from './shared'
const dialectSpecificCodeSnippets: Record = {
postgresql: ` await db.schema.createTable('person')
@@ -17,6 +13,7 @@ const dialectSpecificCodeSnippets: Record = {
.addColumn('created_at', 'timestamp', (cb) =>
cb.notNull().defaultTo(sql\`now()\`)
)
+ .addColumn('metadata', 'jsonb', (cb) => cb.notNull())
.execute()`,
mysql: ` await db.schema.createTable('person')
.addColumn('id', 'integer', (cb) => cb.primaryKey().autoIncrement())
@@ -26,6 +23,7 @@ const dialectSpecificCodeSnippets: Record = {
.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')
@@ -36,6 +34,7 @@ const dialectSpecificCodeSnippets: Record = {
.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())
@@ -45,6 +44,7 @@ const dialectSpecificCodeSnippets: Record = {
.addColumn('created_at', 'timestamp', (cb) =>
cb.notNull().defaultTo(sql\`current_timestamp\`)
)
+ .addColumn('metadata', 'text', (cb) => cb.notNull())
.execute()`,
}
@@ -107,6 +107,12 @@ ${dialectSpecificCodeSnippet}
first_name: 'Jennifer',
last_name: 'Aniston',
gender: 'woman',
+ metadata: sql.valJson({
+ login_at: new Date().toISOString(),
+ ip: null,
+ agent: null,
+ plan: 'free',
+ }),
})
})
diff --git a/site/docs/getting-started/_types.mdx b/site/docs/getting-started/_types.mdx
index 42cf066be..1f4b98b58 100644
--- a/site/docs/getting-started/_types.mdx
+++ b/site/docs/getting-started/_types.mdx
@@ -10,7 +10,7 @@ import {
ColumnType,
Generated,
Insertable,
- JSONColumnType,
+ Json,
Selectable,
Updateable,
} from 'kysely'
@@ -45,12 +45,10 @@ export interface PersonTable {
// can never be updated:
created_at: ColumnType
- // You can specify JSON columns using the `JSONColumnType` wrapper.
- // It is a shorthand for `ColumnType`, 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.valJson` or `sql.valJson`.
+ metadata: Json<{
login_at: string
ip: string | null
agent: string | null
diff --git a/site/docs/recipes/0001-reusable-helpers.md b/site/docs/recipes/0001-reusable-helpers.md
new file mode 100644
index 000000000..2cce66949
--- /dev/null
+++ b/site/docs/recipes/0001-reusable-helpers.md
@@ -0,0 +1,238 @@
+# Reusable helpers
+
+:::info
+[Here's](https://kyse.link/qm67s) a playground link containing all the code in this recipe.
+:::
+
+Let's say you want to write the following query:
+
+```sql
+SELECT id, first_name
+FROM person
+WHERE upper(last_name) = $1
+```
+
+Kysely doesn't have a built-in `upper` function but there are at least three ways you could write this:
+
+```ts
+const lastName = 'STALLONE'
+
+const persons = await db
+ .selectFrom('person')
+ .select(['id', 'first_name'])
+ // 1. `sql` template tag. This is the least type-safe option.
+ // You're providing the column name without any type-checking,
+ // and plugins won't affect it.
+ .where(
+ sql`upper(last_name)`, '=', lastName
+ )
+ // 2. `sql` template tag with `ref`. Anything passed to `ref`
+ // gets type-checked against the accumulated query context.
+ .where(({ eb, ref }) => eb(
+ sql`upper(${ref('last_name')})`, '=', lastName
+ ))
+ // 3. The `fn` function helps you avoid missing parentheses/commas
+ // errors and uses refs as 1st class arguments.
+ .where(({ eb, fn }) => eb(
+ fn('upper', ['last_name']), '=', lastName
+ ))
+ .execute()
+```
+
+but each option could be more readable or type-safe.
+
+Fortunately Kysely allows you to easily create composable, reusable and type-safe helper functions:
+
+```ts
+import { Expression, sql } from 'kysely'
+
+function upper(expr: Expression) {
+ return sql`upper(${expr})`
+}
+
+function lower(expr: Expression) {
+ return sql`lower(${expr})`
+}
+
+function concat(...exprs: Expression[]) {
+ return sql.join(exprs, sql`||`)
+}
+```
+
+Using the `upper` helper, our query would look like this:
+
+```ts
+const lastName = 'STALLONE'
+
+const persons = await db
+ .selectFrom('person')
+ .select(['id', 'first_name'])
+ .where(({ eb, ref }) => eb(
+ upper(ref('last_name')), '=', lastName
+ ))
+ .execute()
+```
+
+The recipe for helper functions is simple: take inputs as `Expression` instances where `T` is the type of the expression. For example `upper` takes in any `string` expression since it transforms strings to upper case. If you implemented the `round` function, it'd take in `Expression` since you can only round numbers.
+
+The helper functions should then use the inputs to create an output that's also an `Expression`. Everything you can create using the expression builder is an instance of `Expression`. So is the output of the `sql` template tag and all methods under the `sql` object. Same goes for `SelectQueryBuilder` and pretty much everything else in Kysely. Everything's an expression.
+
+See [this recipe](https://kysely.dev/docs/recipes/expressions) to learn more about expressions.
+
+So we've learned that everything's an expression and that expressions are composable. Let's put this idea to use:
+
+```ts
+const persons = await db
+ .selectFrom('person')
+ .select(['id', 'first_name'])
+ .where(({ eb, ref, val }) => eb(
+ concat(
+ lower(ref('first_name')),
+ val(' '),
+ upper(ref('last_name'))
+ ),
+ '=',
+ 'sylvester STALLONE'
+ ))
+ .execute()
+```
+
+So far we've only used our helper functions in the first argument of `where` but you can use them anywhere:
+
+```ts
+const persons = await db
+ .selectFrom('person')
+ .innerJoin('pet', (join) => join.on(eb => eb(
+ 'person.first_name', '=', lower(eb.ref('pet.name'))
+ )))
+ .select(({ ref, val }) => [
+ 'first_name',
+ // If you use a helper in `select`, you need to always provide an explicit
+ // name for it using the `as` method.
+ concat(ref('person.first_name'), val(' '), ref('pet.name')).as('name_with_pet')
+ ])
+ .orderBy(({ ref }) => lower(ref('first_name')))
+ .execute()
+```
+
+## Reusable helpers using `ExpressionBuilder`
+
+Here's an example of a helper function that uses the expression builder instead of raw SQL:
+
+```ts
+import { Expression, expressionBuilder } from 'kysely'
+
+function idsOfPersonsThatHaveDogNamed(name: Expression) {
+ const eb = expressionBuilder()
+
+ // A subquery that returns the identifiers of all persons
+ // that have a dog named `name`.
+ return eb
+ .selectFrom('pet')
+ .select('pet.owner_id')
+ .where('pet.species', '=', 'dog')
+ .where('pet.name', '=', name)
+}
+```
+
+And here's how you could use it:
+
+```ts
+const dogName = 'Doggo'
+
+const persons = await db
+ .selectFrom('person')
+ .selectAll('person')
+ .where((eb) => eb(
+ 'person.id', 'in', idsOfPersonsThatHaveDogNamed(eb.val(dogName))
+ ))
+ .execute()
+```
+
+Note that `idsOfPersonsThatHaveDogNamed` doesn't execute a separate query but instead returns a subquery expression that's compiled as a part of the parent query:
+
+```sql
+select
+ person.*
+from
+ person
+where
+ person.id in (
+ select pet.owner_id
+ from pet
+ where pet.species = 'dog'
+ and pet.name = ?
+ )
+```
+
+In all our examples we've used the following syntax:
+
+```ts
+.where(eb => eb(left, operator, right))
+```
+
+When the expression builder `eb` is used as a function, it creates a binary expression. All binary expressions with a comparison operator are represented as a `Expression`. You don't always need to return `eb(left, operator, right)` from the callback though. Since `Expressions` are composable and reusable, you can return any `Expression`.
+
+This means you can create helpers like this:
+
+```ts
+function isOlderThan(age: Expression) {
+ return sql`age > ${age}`
+}
+```
+
+```ts
+const persons = await db
+ .selectFrom('person')
+ .select(['id', 'first_name'])
+ .where(({ val }) => isOlderThan(val(60)))
+ .execute()
+```
+
+## Dealing with nullable expressions
+
+If you want your helpers to work with nullable expressions (nullable columns etc.), you can do something like this:
+
+```ts
+import { Expression } from 'kysely'
+
+// This function accepts both nullable and non-nullable string expressions.
+function toInt(expr: Expression) {
+ // This returns `Expression` if `expr` is nullable
+ // and `Expression` otherwise.
+ return sql`(${expr})::integer`
+}
+```
+
+## Passing select queries as expressions
+
+Let's say we have the following query:
+
+```ts
+const expr: Expression<{ name: string }> = db
+ .selectFrom('pet')
+ .select('pet.name')
+```
+
+The expression type of our query is `Expression<{ name: string }>` but SQL allows you to use a query like that as an `Expression`. In other words, SQL allows you to use single-column record types like scalars. Most of the time Kysely is able to automatically handle this case but with helper functions you need to use `$asScalar()` to convert the type. Here's an example:
+
+```ts
+const persons = await db
+ .selectFrom('person')
+ .select((eb) => [
+ 'id',
+ 'first_name',
+ upper(
+ eb.selectFrom('pet')
+ .select('name')
+ .whereRef('person.id', '=', 'pet.owner_id')
+ .limit(1)
+ .$asScalar() // <-- This is needed
+ .$notNull()
+ ).as('pet_name')
+ ])
+```
+
+The subquery is an `Expression<{ name: string }>` but our `upper` function only accepts `Expression`. That's why we need to call `$asScalar()`. `$asScalar()` has no effect on the generated SQL. It's simply a type-level helper.
+
+We also used `$notNull()` in the example because our simple `upper` function doesn't support nullable expressions.
\ No newline at end of file
diff --git a/site/docs/recipes/0006-expressions.md b/site/docs/recipes/0006-expressions.md
index 3ac86cead..154eb88a6 100644
--- a/site/docs/recipes/0006-expressions.md
+++ b/site/docs/recipes/0006-expressions.md
@@ -6,9 +6,9 @@ An [`Expression`](https://kysely-org.github.io/kysely-apidoc/interfaces/Expre
## Expression builder
-Expressions are usually built using an instance of [`ExpressionBuilder`](https://kysely-org.github.io/kysely-apidoc/interfaces/ExpressionBuilder.html). `DB` is the same database type you give to `Kysely` when you create an instance. `TB` is the union of all table names that are visible in the context. For example `ExpressionBuilder` means you can access `person` and `pet` tables and all their columns in the expression.
+Expressions are usually built using an instance of [`ExpressionBuilder`](https://kysely-org.github.io/kysely-apidoc/interfaces/ExpressionBuilder.html). `DB` is the same database type you give to `Kysely` when you create an instance. `TB` is the union of all table names that are visible in the context. For example `ExpressionBuilder` means you can reference `person` and `pet` columns in the created expressions.
-You can get an instance of the expression builder by using a callback:
+You can get an instance of the expression builder using a callback:
```ts
const person = await db
@@ -28,7 +28,13 @@ const person = await db
.as('pet_name'),
// Select a boolean expression
- eb('first_name', '=', 'Jennifer').as('is_jennifer')
+ eb('first_name', '=', 'Jennifer').as('is_jennifer'),
+
+ // Select a static string value
+ eb.val('Some value').as('string_value'),
+
+ // Select a literal value
+ eb.lit(42).as('literal_value'),
])
// You can also destructure the expression builder like this
.where(({ and, or, eb, not, exists, selectFrom }) => or([
@@ -63,19 +69,21 @@ select
limit 1
) as "pet_name",
- "first_name" = $1 as "is_jennifer"
+ "first_name" = $1 as "is_jennifer",
+ $2 as "string_value",
+ 42 as "literal_value"
from
"person"
where (
(
- "first_name" = $2
- and "last_name" = $3
+ "first_name" = $3
+ and "last_name" = $4
)
or not exists (
select "pet.id"
from "pet"
where "pet"."owner_id" = "person"."id"
- and "pet"."species" in ($4, $5)
+ and "pet"."species" in ($5, $6)
)
)
```
@@ -91,11 +99,17 @@ There's also a global function `expressionBuilder` you can use to create express
```ts
import { expressionBuilder } from 'kysely'
-// `eb1` has type `ExpressionBuilder`
-const eb1 = expressionBuilder()
+// `eb1` has type `ExpressionBuilder` which means there are no tables in the
+// context. This variant should be used most of the time in helper functions since you
+// shouldn't make assumptions about the calling context.
+const eb1 = expressionBuilder()
+
+// `eb2` has type `ExpressionBuilder`. You can reference `person` columns
+// directly in all expression builder methods.
+const eb2 = expressionBuilder()
// In this one you'd have access to tables `person` and `pet` and all their columns.
-const eb2 = expressionBuilder()
+const eb3 = expressionBuilder()
let qb = query
.selectFrom('person')
@@ -141,7 +155,7 @@ const doggoPersons = await db
.execute()
```
-The above helper is not very type-safe. The following code would compile, but fail at runtime:
+However, the above helper is not very type-safe. The following code would compile, but fail at runtime:
```ts
const bigFatFailure = await db
@@ -160,7 +174,7 @@ in arbitrary expressions instead of just values.
function hasDogNamed(name: Expression, ownerId: Expression) {
// Create an expression builder without any tables in the context.
// This way we make no assumptions about the calling context.
- const eb = expressionBuilder()
+ const eb = expressionBuilder()
return eb.exists(
eb.selectFrom('pet')
@@ -182,11 +196,13 @@ const doggoPersons = await db
.execute()
```
+Learn more about reusable helper functions [here](https://kysely.dev/docs/recipes/reusable-helpers).
+
## Conditional expressions
In the following, we'll only cover `where` expressions. The same logic applies to `having`, `on`, `orderBy`, `groupBy` etc.
-> This section should not be confused with conditional selections in `select` clauses, which is a whole 'nother topic we discuss in [this recipe](https://www.kysely.dev/docs/recipes/conditional-selects).
+> This section should not be confused with conditional selections in `select` clauses, which is a whole 'nother topic we discuss in [this recipe](https://kysely.dev/docs/recipes/conditional-selects).
Having a set of optional filters you want to combine using `and`, is the most basic and common use case of conditional `where` expressions.
Since the `where`, `having` and other filter functions are additive, most of the time this is enough:
diff --git a/src/dialect/mssql/mssql-dialect-config.ts b/src/dialect/mssql/mssql-dialect-config.ts
index 197610ba3..e4af5ab48 100644
--- a/src/dialect/mssql/mssql-dialect-config.ts
+++ b/src/dialect/mssql/mssql-dialect-config.ts
@@ -67,17 +67,20 @@ export interface Tedious {
export interface TediousConnection {
beginTransaction(
- callback: (error?: Error | null, transactionDescriptor?: any) => void,
- name?: string,
- isolationLevel?: number,
+ callback: (
+ err: Error | null | undefined,
+ transactionDescriptor?: any,
+ ) => void,
+ name?: string | undefined,
+ isolationLevel?: number | undefined,
): void
cancel(): boolean
close(): void
commitTransaction(
- callback: (error?: Error | null) => void,
- name?: string,
+ callback: (err: Error | null | undefined) => void,
+ name?: string | undefined,
): void
- connect(callback?: (error?: Error) => void): void
+ connect(connectListener: (err?: Error) => void): void
execSql(request: TediousRequest): void
off(event: 'error', listener: (error: unknown) => void): this
off(event: string, listener: (...args: any[]) => void): this
@@ -85,12 +88,15 @@ export interface TediousConnection {
on(event: string, listener: (...args: any[]) => void): this
once(event: 'end', listener: () => void): this
once(event: string, listener: (...args: any[]) => void): this
- reset(callback: (error?: Error | null) => void): void
+ reset(callback: (err: Error | null | undefined) => void): void
rollbackTransaction(
- callback: (error?: Error | null) => void,
- name?: string,
+ callback: (err: Error | null | undefined) => void,
+ name?: string | undefined,
+ ): void
+ saveTransaction(
+ callback: (err: Error | null | undefined) => void,
+ name: string,
): void
- saveTransaction(callback: (error?: Error | null) => void, name: string): void
}
export type TediousIsolationLevel = Record
diff --git a/src/dialect/mssql/mssql-driver.ts b/src/dialect/mssql/mssql-driver.ts
index b9d44e203..8ba60841e 100644
--- a/src/dialect/mssql/mssql-driver.ts
+++ b/src/dialect/mssql/mssql-driver.ts
@@ -86,6 +86,20 @@ export class MssqlDriver implements Driver {
await connection.rollbackTransaction()
}
+ async savepoint(
+ connection: MssqlConnection,
+ savepointName: string,
+ ): Promise {
+ await connection.savepoint(savepointName)
+ }
+
+ async rollbackToSavepoint(
+ connection: MssqlConnection,
+ savepointName: string,
+ ): Promise {
+ await connection.rollbackTransaction(savepointName)
+ }
+
async releaseConnection(connection: MssqlConnection): Promise {
await connection[PRIVATE_RELEASE_METHOD]()
this.#pool.release(connection)
@@ -174,12 +188,21 @@ class MssqlConnection implements DatabaseConnection {
}
}
- async rollbackTransaction(): Promise {
+ async rollbackTransaction(savepointName?: string): Promise {
await new Promise((resolve, reject) =>
this.#connection.rollbackTransaction((error) => {
if (error) reject(error)
else resolve(undefined)
- }),
+ }, savepointName),
+ )
+ }
+
+ async savepoint(savepointName: string): Promise {
+ await new Promise((resolve, reject) =>
+ this.#connection.saveTransaction((error) => {
+ if (error) reject(error)
+ else resolve(undefined)
+ }, savepointName),
)
}
diff --git a/src/dialect/mysql/mysql-driver.ts b/src/dialect/mysql/mysql-driver.ts
index 063d84ae0..084b6cb6f 100644
--- a/src/dialect/mysql/mysql-driver.ts
+++ b/src/dialect/mysql/mysql-driver.ts
@@ -3,7 +3,9 @@ import {
QueryResult,
} from '../../driver/database-connection.js'
import { Driver, TransactionSettings } from '../../driver/driver.js'
+import { parseSavepointCommand } from '../../parser/savepoint-parser.js'
import { CompiledQuery } from '../../query-compiler/compiled-query.js'
+import { QueryCompiler } from '../../query-compiler/query-compiler.js'
import { isFunction, isObject, freeze } from '../../util/object-utils.js'
import { extendStackTrace } from '../../util/stack-trace-utils.js'
import {
@@ -90,6 +92,36 @@ export class MysqlDriver implements Driver {
await connection.executeQuery(CompiledQuery.raw('rollback'))
}
+ async savepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('savepoint', savepointName)),
+ )
+ }
+
+ async rollbackToSavepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('rollback to', savepointName)),
+ )
+ }
+
+ async releaseSavepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('release savepoint', savepointName)),
+ )
+ }
+
async releaseConnection(connection: MysqlConnection): Promise {
connection[PRIVATE_RELEASE_METHOD]()
}
diff --git a/src/dialect/postgres/postgres-driver.ts b/src/dialect/postgres/postgres-driver.ts
index 7b6639fe2..0321e548b 100644
--- a/src/dialect/postgres/postgres-driver.ts
+++ b/src/dialect/postgres/postgres-driver.ts
@@ -3,7 +3,14 @@ import {
QueryResult,
} from '../../driver/database-connection.js'
import { Driver, TransactionSettings } from '../../driver/driver.js'
+import { IdentifierNode } from '../../operation-node/identifier-node.js'
+import { RawNode } from '../../operation-node/raw-node.js'
+import { parseSavepointCommand } from '../../parser/savepoint-parser.js'
import { CompiledQuery } from '../../query-compiler/compiled-query.js'
+import {
+ QueryCompiler,
+ RootOperationNode,
+} from '../../query-compiler/query-compiler.js'
import { isFunction, freeze } from '../../util/object-utils.js'
import { extendStackTrace } from '../../util/stack-trace-utils.js'
import {
@@ -78,6 +85,36 @@ export class PostgresDriver implements Driver {
await connection.executeQuery(CompiledQuery.raw('rollback'))
}
+ async savepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('savepoint', savepointName)),
+ )
+ }
+
+ async rollbackToSavepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('rollback to', savepointName)),
+ )
+ }
+
+ async releaseSavepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('release', savepointName)),
+ )
+ }
+
async releaseConnection(connection: PostgresConnection): Promise {
connection[PRIVATE_RELEASE_METHOD]()
}
diff --git a/src/dialect/sqlite/sqlite-driver.ts b/src/dialect/sqlite/sqlite-driver.ts
index dcfbca2e6..5aefb32ef 100644
--- a/src/dialect/sqlite/sqlite-driver.ts
+++ b/src/dialect/sqlite/sqlite-driver.ts
@@ -4,7 +4,9 @@ import {
} from '../../driver/database-connection.js'
import { Driver } from '../../driver/driver.js'
import { SelectQueryNode } from '../../operation-node/select-query-node.js'
+import { parseSavepointCommand } from '../../parser/savepoint-parser.js'
import { CompiledQuery } from '../../query-compiler/compiled-query.js'
+import { QueryCompiler } from '../../query-compiler/query-compiler.js'
import { freeze, isFunction } from '../../util/object-utils.js'
import { SqliteDatabase, SqliteDialectConfig } from './sqlite-dialect-config.js'
@@ -50,6 +52,36 @@ export class SqliteDriver implements Driver {
await connection.executeQuery(CompiledQuery.raw('rollback'))
}
+ async savepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('savepoint', savepointName)),
+ )
+ }
+
+ async rollbackToSavepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('rollback to', savepointName)),
+ )
+ }
+
+ async releaseSavepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ await connection.executeQuery(
+ compileQuery(parseSavepointCommand('release', savepointName)),
+ )
+ }
+
async releaseConnection(): Promise {
this.#connectionMutex.unlock()
}
diff --git a/src/driver/driver.ts b/src/driver/driver.ts
index ef9214d17..849cd0129 100644
--- a/src/driver/driver.ts
+++ b/src/driver/driver.ts
@@ -1,3 +1,4 @@
+import { QueryCompiler } from '../query-compiler/query-compiler.js'
import { ArrayItemType } from '../util/type-utils.js'
import { DatabaseConnection } from './database-connection.js'
@@ -37,6 +38,33 @@ export interface Driver {
*/
rollbackTransaction(connection: DatabaseConnection): Promise
+ /**
+ * Establishses a new savepoint within a transaction.
+ */
+ savepoint?(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise
+
+ /**
+ * Rolls back to a savepoint within a transaction.
+ */
+ rollbackToSavepoint?(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise
+
+ /**
+ * Releases a savepoint within a transaction.
+ */
+ releaseSavepoint?(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise
+
/**
* Releases a connection back to the pool.
*/
diff --git a/src/driver/dummy-driver.ts b/src/driver/dummy-driver.ts
index 4a4d00fe6..7a457c8bb 100644
--- a/src/driver/dummy-driver.ts
+++ b/src/driver/dummy-driver.ts
@@ -65,6 +65,18 @@ export class DummyDriver implements Driver {
async destroy(): Promise {
// Nothing to do here.
}
+
+ async releaseSavepoint(): Promise {
+ // Nothing to do here.
+ }
+
+ async rollbackToSavepoint(): Promise {
+ // Nothing to do here.
+ }
+
+ async savepoint(): Promise {
+ // Nothing to do here.
+ }
}
class DummyConnection implements DatabaseConnection {
diff --git a/src/driver/runtime-driver.ts b/src/driver/runtime-driver.ts
index a7ba8d771..d05467ccf 100644
--- a/src/driver/runtime-driver.ts
+++ b/src/driver/runtime-driver.ts
@@ -1,4 +1,5 @@
import { CompiledQuery } from '../query-compiler/compiled-query.js'
+import { QueryCompiler } from '../query-compiler/query-compiler.js'
import { Log } from '../util/log.js'
import { performanceNow } from '../util/performance-now.js'
import { DatabaseConnection, QueryResult } from './database-connection.js'
@@ -85,6 +86,54 @@ export class RuntimeDriver implements Driver {
return this.#driver.rollbackTransaction(connection)
}
+ savepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ if (this.#driver.savepoint) {
+ return this.#driver.savepoint(connection, savepointName, compileQuery)
+ }
+
+ throw new Error('The `savepoint` method is not supported by this driver')
+ }
+
+ rollbackToSavepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ if (this.#driver.rollbackToSavepoint) {
+ return this.#driver.rollbackToSavepoint(
+ connection,
+ savepointName,
+ compileQuery,
+ )
+ }
+
+ throw new Error(
+ 'The `rollbackToSavepoint` method is not supported by this driver',
+ )
+ }
+
+ releaseSavepoint(
+ connection: DatabaseConnection,
+ savepointName: string,
+ compileQuery: QueryCompiler['compileQuery'],
+ ): Promise {
+ if (this.#driver.releaseSavepoint) {
+ return this.#driver.releaseSavepoint(
+ connection,
+ savepointName,
+ compileQuery,
+ )
+ }
+
+ throw new Error(
+ 'The `releaseSavepoint` method is not supported by this driver',
+ )
+ }
+
async destroy(): Promise {
if (!this.#initPromise) {
return
diff --git a/src/expression/expression-builder.ts b/src/expression/expression-builder.ts
index c3faa4c57..ea760bf2e 100644
--- a/src/expression/expression-builder.ts
+++ b/src/expression/expression-builder.ts
@@ -5,13 +5,7 @@ import {
import { SelectQueryNode } from '../operation-node/select-query-node.js'
import {
parseTableExpressionOrList,
- TableExpression,
- From,
TableExpressionOrList,
- FromTables,
- ExtractTableAlias,
- AnyAliasedTable,
- PickTableWithAlias,
parseTable,
} from '../parser/table-parser.js'
import { WithSchemaPlugin } from '../plugin/with-schema/with-schema-plugin.js'
@@ -75,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 {
@@ -83,6 +77,8 @@ import {
parseDataTypeExpression,
} 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 {
/**
@@ -151,7 +147,7 @@ export interface ExpressionBuilder {
* eb.selectFrom('person')
* .selectAll()
* .where((eb) => eb(
- * eb.fn('lower', ['first_name']),
+ * eb.fn('lower', ['first_name']),
* 'in',
* eb.selectFrom('pet')
* .select('pet.name')
@@ -182,12 +178,12 @@ export interface ExpressionBuilder {
*
* ```ts
* db.selectFrom('person')
+ * .selectAll()
* .where(({ eb, exists, selectFrom }) =>
- * eb('first_name', '=', 'Jennifer').and(
- * exists(selectFrom('pet').whereRef('owner_id', '=', 'person.id').select('pet.id'))
- * )
+ * eb('first_name', '=', 'Jennifer').and(exists(
+ * selectFrom('pet').whereRef('owner_id', '=', 'person.id').select('pet.id')
+ * ))
* )
- * .selectAll()
* ```
*
* The generated SQL (PostgreSQL):
@@ -277,29 +273,9 @@ export interface ExpressionBuilder {
* that case Kysely typings wouldn't allow you to reference `pet.owner_id`
* because `pet` is not joined to that query.
*/
- selectFrom(
- from: TE[],
- ): SelectQueryBuilder, {}>
-
- selectFrom>(
- from: TE[],
- ): SelectQueryBuilder, FromTables, {}>
-
- selectFrom(
- from: TE,
- ): SelectQueryBuilder, {}>
-
- selectFrom>(
+ selectFrom>(
from: TE,
- ): SelectQueryBuilder<
- DB & PickTableWithAlias,
- TB | ExtractTableAlias, TE>,
- {}
- >
-
- selectFrom>(
- from: TE,
- ): SelectQueryBuilder, FromTables, {}>
+ ): SelectFrom
/**
* Creates a `case` statement/operator.
@@ -536,6 +512,44 @@ export interface ExpressionBuilder {
value: VE,
): ExpressionWrapper>
+ /**
+ * 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 { GeneratedAlways, Json } from 'kysely'
+ *
+ * interface Database {
+ * person: {
+ * id: GeneratedAlways
+ * name: string
+ * experience: Json<{ title: string; company: string }[]>
+ * preferences: Json<{ locale: string; timezone: string }>
+ * profile: Json<{ email_verified: boolean }>
+ * }
+ * }
+ *
+ * const result = await db
+ * .insertInto('person')
+ * .values(({ valJson }) => ({
+ * name: 'Jennifer Aniston',
+ * experience: valJson([{ title: 'Software Engineer', company: 'Google' }]), // ✔️
+ * preferences: valJson({ locale: 'en' }), // ❌ missing `timezone`
+ * profile: JSON.stringify({ email_verified: true }), // ❌ doesn't match `Serialized<{ email_verified }>`
+ * }))
+ * .execute()
+ * ```
+ */
+ valJson(
+ obj: O,
+ ): ExpressionWrapper>
+
/**
* Creates a tuple expression.
*
@@ -1165,6 +1179,14 @@ export function createExpressionBuilder(
return new ExpressionWrapper(parseValueExpression(value))
},
+ valJson(
+ value: O,
+ ): ExpressionWrapper> {
+ return new ExpressionWrapper(
+ ValueNode.create(value, { serialized: true }),
+ )
+ },
+
refTuple(
...values: ReadonlyArray>
): ExpressionWrapper {
@@ -1311,10 +1333,10 @@ export function expressionBuilder(
_: SelectQueryBuilder,
): ExpressionBuilder
-export function expressionBuilder(): ExpressionBuilder<
+export function expressionBuilder<
DB,
- TB
->
+ TB extends keyof DB = never,
+>(): ExpressionBuilder
export function expressionBuilder(
_?: unknown,
diff --git a/src/index.ts b/src/index.ts
index 9818f1131..b467d871c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -53,6 +53,7 @@ export * from './schema/column-definition-builder.js'
export * from './schema/foreign-key-constraint-builder.js'
export * from './schema/alter-table-builder.js'
export * from './schema/create-view-builder.js'
+export * from './schema/refresh-materialized-view-builder.js'
export * from './schema/drop-view-builder.js'
export * from './schema/alter-column-builder.js'
@@ -133,6 +134,7 @@ export * from './operation-node/create-schema-node.js'
export * from './operation-node/create-table-node.js'
export * from './operation-node/create-type-node.js'
export * from './operation-node/create-view-node.js'
+export * from './operation-node/refresh-materialized-view-node.js'
export * from './operation-node/data-type-node.js'
export * from './operation-node/default-insert-value-node.js'
export * from './operation-node/default-value-node.js'
diff --git a/src/kysely.ts b/src/kysely.ts
index 987c8947e..7676c0336 100644
--- a/src/kysely.ts
+++ b/src/kysely.ts
@@ -16,7 +16,6 @@ import {
TransactionSettings,
TRANSACTION_ISOLATION_LEVELS,
} from './driver/driver.js'
-import { preventAwait } from './util/prevent-await.js'
import {
createFunctionModule,
FunctionModule,
@@ -33,6 +32,18 @@ import { parseExpression } from './parser/expression-parser.js'
import { Expression } from './expression/expression.js'
import { WithSchemaPlugin } from './plugin/with-schema/with-schema-plugin.js'
import { DrainOuterGeneric } from './util/type-utils.js'
+import { QueryCompiler } from './query-compiler/query-compiler.js'
+import {
+ ReleaseSavepoint,
+ RollbackToSavepoint,
+} from './parser/savepoint-parser.js'
+import {
+ ControlledConnection,
+ provideControlledConnection,
+} from './util/provide-controlled-connection.js'
+
+// @ts-ignore
+Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose')
/**
* The main Kysely class.
@@ -79,7 +90,7 @@ import { DrainOuterGeneric } from './util/type-utils.js'
*/
export class Kysely
extends QueryCreator
- implements QueryExecutorProvider
+ implements QueryExecutorProvider, AsyncDisposable
{
readonly #props: KyselyProps
@@ -211,6 +222,9 @@ export class Kysely
* of type {@link Transaction} which inherits {@link Kysely}. Any query
* started through the transaction object is executed inside the transaction.
*
+ * To run a controlled transaction, allowing you to commit and rollback manually,
+ * use {@link startTransaction} instead.
+ *
* ### Examples
*
*
@@ -260,6 +274,122 @@ export class Kysely
return new TransactionBuilder({ ...this.#props })
}
+ /**
+ * Creates a {@link ControlledTransactionBuilder} that can be used to run queries inside a controlled transaction.
+ *
+ * The returned {@link ControlledTransactionBuilder} can be used to configure the transaction.
+ * The {@link ControlledTransactionBuilder.execute} method can then be called
+ * to start the transaction and return a {@link ControlledTransaction}.
+ *
+ * A {@link ControlledTransaction} allows you to commit and rollback manually,
+ * execute savepoint commands. It extends {@link Transaction} which extends {@link Kysely},
+ * so you can run queries inside the transaction. Once the transaction is committed,
+ * or rolled back, it can't be used anymore - all queries will throw an error.
+ * This is to prevent accidentally running queries outside the transaction - where
+ * atomicity is not guaranteed anymore.
+ *
+ * ### Examples
+ *
+ *
+ *
+ * A controlled transaction allows you to commit and rollback manually, execute
+ * savepoint commands, and queries in general.
+ *
+ * In this example we start a transaction, use it to insert two rows and then commit
+ * the transaction. If an error is thrown, we catch it and rollback the transaction.
+ *
+ * ```ts
+ * const trx = await db.startTransaction().execute()
+ *
+ * try {
+ * const jennifer = await trx.insertInto('person')
+ * .values({
+ * first_name: 'Jennifer',
+ * last_name: 'Aniston',
+ * age: 40,
+ * })
+ * .returning('id')
+ * .executeTakeFirstOrThrow()
+ *
+ * const catto = await trx.insertInto('pet')
+ * .values({
+ * owner_id: jennifer.id,
+ * name: 'Catto',
+ * species: 'cat',
+ * is_favorite: false,
+ * })
+ * .returningAll()
+ * .executeTakeFirstOrThrow()
+ *
+ * await trx.commit().execute()
+ *
+ * return catto
+ * } catch (error) {
+ * await trx.rollback().execute()
+ * }
+ * ```
+ *
+ *
+ *
+ * A controlled transaction allows you to commit and rollback manually, execute
+ * savepoint commands, and queries in general.
+ *
+ * In this example we start a transaction, insert a person, create a savepoint,
+ * try inserting a toy and a pet, and if an error is thrown, we rollback to the
+ * savepoint. Eventually we release the savepoint, insert an audit record and
+ * commit the transaction. If an error is thrown, we catch it and rollback the
+ * transaction.
+ *
+ * ```ts
+ * const trx = await db.startTransaction().execute()
+ *
+ * try {
+ * const jennifer = await trx
+ * .insertInto('person')
+ * .values({
+ * first_name: 'Jennifer',
+ * last_name: 'Aniston',
+ * age: 40,
+ * })
+ * .returning('id')
+ * .executeTakeFirstOrThrow()
+ *
+ * const trxAfterJennifer = await trx.savepoint('after_jennifer').execute()
+ *
+ * try {
+ * const bone = await trxAfterJennifer
+ * .insertInto('toy')
+ * .values({ name: 'Bone', price: 1.99 })
+ * .returning('id')
+ * .executeTakeFirstOrThrow()
+ *
+ * await trxAfterJennifer
+ * .insertInto('pet')
+ * .values({
+ * owner_id: jennifer.id,
+ * name: 'Catto',
+ * species: 'cat',
+ * favorite_toy_id: bone.id,
+ * })
+ * .execute()
+ * } catch (error) {
+ * await trxAfterJennifer.rollbackToSavepoint('after_jennifer').execute()
+ * }
+ *
+ * await trxAfterJennifer.releaseSavepoint('after_jennifer').execute()
+ *
+ * await trx.insertInto('audit').values({ action: 'added Jennifer' }).execute()
+ *
+ * await trx.commit().execute()
+ * } catch (error) {
+ * await trx.rollback().execute()
+ * }
+ * ```
+ */
+ startTransaction(): ControlledTransactionBuilder {
+ return new ControlledTransactionBuilder({ ...this.#props })
+ }
+
/**
* Provides a kysely instance bound to a single database connection.
*
@@ -388,6 +518,10 @@ export class Kysely
return this.getExecutor().executeQuery(compiledQuery, queryId)
}
+
+ async [Symbol.asyncDispose]() {
+ await this.destroy()
+ }
}
export class Transaction extends Kysely {
@@ -532,11 +666,6 @@ export class ConnectionBuilder {
interface ConnectionBuilderProps extends KyselyProps {}
-preventAwait(
- ConnectionBuilder,
- "don't await ConnectionBuilder instances directly. To execute the query you need to call the `execute` method",
-)
-
export class TransactionBuilder {
readonly #props: TransactionBuilderProps
@@ -585,11 +714,6 @@ interface TransactionBuilderProps extends KyselyProps {
readonly isolationLevel?: IsolationLevel
}
-preventAwait(
- TransactionBuilder,
- "don't await TransactionBuilder instances directly. To execute the transaction you need to call the `execute` method",
-)
-
function validateTransactionSettings(settings: TransactionSettings): void {
if (
settings.isolationLevel &&
@@ -600,3 +724,325 @@ function validateTransactionSettings(settings: TransactionSettings): void {
)
}
}
+
+export class ControlledTransactionBuilder {
+ readonly #props: ControlledTransactionBuilderProps
+
+ constructor(props: ControlledTransactionBuilderProps) {
+ this.#props = freeze(props)
+ }
+
+ setIsolationLevel(
+ isolationLevel: IsolationLevel,
+ ): ControlledTransactionBuilder {
+ return new ControlledTransactionBuilder({
+ ...this.#props,
+ isolationLevel,
+ })
+ }
+
+ async execute(): Promise> {
+ const { isolationLevel, ...props } = this.#props
+ const settings = { isolationLevel }
+
+ validateTransactionSettings(settings)
+
+ const connection = await provideControlledConnection(this.#props.executor)
+
+ await this.#props.driver.beginTransaction(connection, settings)
+
+ return new ControlledTransaction({
+ ...props,
+ connection,
+ executor: this.#props.executor.withConnectionProvider(
+ new SingleConnectionProvider(connection),
+ ),
+ })
+ }
+}
+
+interface ControlledTransactionBuilderProps extends TransactionBuilderProps {
+ readonly releaseConnection?: boolean
+}
+
+export class ControlledTransaction<
+ DB,
+ S extends string[] = [],
+> extends Transaction {
+ readonly #props: ControlledTransactionProps
+ readonly #compileQuery: QueryCompiler['compileQuery']
+ #isCommitted: boolean
+ #isRolledBack: boolean
+
+ constructor(props: ControlledTransactionProps) {
+ const { connection, ...transactionProps } = props
+ super(transactionProps)
+ this.#props = freeze(props)
+
+ const queryId = createQueryId()
+ this.#compileQuery = (node) => props.executor.compileQuery(node, queryId)
+
+ this.#isCommitted = false
+ this.#isRolledBack = false
+
+ this.#assertNotCommittedOrRolledBackBeforeAllExecutions()
+ }
+
+ get isCommitted(): boolean {
+ return this.#isCommitted
+ }
+
+ get isRolledBack(): boolean {
+ return this.#isRolledBack
+ }
+
+ /**
+ * Commits the transaction.
+ *
+ * See {@link rollback}.
+ *
+ * ### Examples
+ *
+ * ```ts
+ * try {
+ * await doSomething(trx)
+ *
+ * await trx.commit().execute()
+ * } catch (error) {
+ * await trx.rollback().execute()
+ * }
+ * ```
+ */
+ commit(): Command {
+ this.#assertNotCommittedOrRolledBack()
+
+ return new Command(async () => {
+ await this.#props.driver.commitTransaction(this.#props.connection)
+ this.#isCommitted = true
+ this.#props.connection.release()
+ })
+ }
+
+ /**
+ * Rolls back the transaction.
+ *
+ * See {@link commit} and {@link rollbackToSavepoint}.
+ *
+ * ### Examples
+ *
+ * ```ts
+ * try {
+ * await doSomething(trx)
+ *
+ * await trx.commit().execute()
+ * } catch (error) {
+ * await trx.rollback().execute()
+ * }
+ * ```
+ */
+ rollback(): Command {
+ this.#assertNotCommittedOrRolledBack()
+
+ return new Command(async () => {
+ await this.#props.driver.rollbackTransaction(this.#props.connection)
+ this.#isRolledBack = true
+ this.#props.connection.release()
+ })
+ }
+
+ /**
+ * Creates a savepoint with a given name.
+ *
+ * See {@link rollbackToSavepoint} and {@link releaseSavepoint}.
+ *
+ * For a type-safe experience, you should use the returned instance from now on.
+ *
+ * ### Examples
+ *
+ * ```ts
+ * await insertJennifer(trx)
+ *
+ * const trxAfterJennifer = await trx.savepoint('after_jennifer').execute()
+ *
+ * try {
+ * await doSomething(trxAfterJennifer)
+ * } catch (error) {
+ * await trxAfterJennifer.rollbackToSavepoint('after_jennifer').execute()
+ * }
+ * ```
+ */
+ savepoint(
+ savepointName: SN extends S ? never : SN,
+ ): Command> {
+ this.#assertNotCommittedOrRolledBack()
+
+ return new Command(async () => {
+ await this.#props.driver.savepoint?.(
+ this.#props.connection,
+ savepointName,
+ this.#compileQuery,
+ )
+
+ return new ControlledTransaction({ ...this.#props })
+ })
+ }
+
+ /**
+ * Rolls back to a savepoint with a given name.
+ *
+ * See {@link savepoint} and {@link releaseSavepoint}.
+ *
+ * You must use the same instance returned by {@link savepoint}, or
+ * escape the type-check by using `as any`.
+ *
+ * ### Examples
+ *
+ * ```ts
+ * await insertJennifer(trx)
+ *
+ * const trxAfterJennifer = await trx.savepoint('after_jennifer').execute()
+ *
+ * try {
+ * await doSomething(trxAfterJennifer)
+ * } catch (error) {
+ * await trxAfterJennifer.rollbackToSavepoint('after_jennifer').execute()
+ * }
+ * ```
+ */
+ rollbackToSavepoint(
+ savepointName: SN,
+ ): RollbackToSavepoint extends string[]
+ ? Command>>
+ : never {
+ this.#assertNotCommittedOrRolledBack()
+
+ return new Command(async () => {
+ await this.#props.driver.rollbackToSavepoint?.(
+ this.#props.connection,
+ savepointName,
+ this.#compileQuery,
+ )
+
+ return new ControlledTransaction({ ...this.#props })
+ }) as any
+ }
+
+ /**
+ * Releases a savepoint with a given name.
+ *
+ * See {@link savepoint} and {@link rollbackToSavepoint}.
+ *
+ * You must use the same instance returned by {@link savepoint}, or
+ * escape the type-check by using `as any`.
+ *
+ * ### Examples
+ *
+ * ```ts
+ * await insertJennifer(trx)
+ *
+ * const trxAfterJennifer = await trx.savepoint('after_jennifer').execute()
+ *
+ * try {
+ * await doSomething(trxAfterJennifer)
+ * } catch (error) {
+ * await trxAfterJennifer.rollbackToSavepoint('after_jennifer').execute()
+ * }
+ *
+ * await trxAfterJennifer.releaseSavepoint('after_jennifer').execute()
+ *
+ * await doSomethingElse(trx)
+ * ```
+ */
+ releaseSavepoint(
+ savepointName: SN,
+ ): ReleaseSavepoint extends string[]
+ ? Command>>
+ : never {
+ this.#assertNotCommittedOrRolledBack()
+
+ return new Command(async () => {
+ await this.#props.driver.releaseSavepoint?.(
+ this.#props.connection,
+ savepointName,
+ this.#compileQuery,
+ )
+
+ return new ControlledTransaction({ ...this.#props })
+ }) as any
+ }
+
+ override withPlugin(plugin: KyselyPlugin): ControlledTransaction {
+ return new ControlledTransaction({
+ ...this.#props,
+ executor: this.#props.executor.withPlugin(plugin),
+ })
+ }
+
+ override withoutPlugins(): ControlledTransaction {
+ return new ControlledTransaction({
+ ...this.#props,
+ executor: this.#props.executor.withoutPlugins(),
+ })
+ }
+
+ override withSchema(schema: string): ControlledTransaction {
+ return new ControlledTransaction({
+ ...this.#props,
+ executor: this.#props.executor.withPluginAtFront(
+ new WithSchemaPlugin(schema),
+ ),
+ })
+ }
+
+ override withTables<
+ T extends Record>,
+ >(): ControlledTransaction, S> {
+ return new ControlledTransaction({ ...this.#props })
+ }
+
+ #assertNotCommittedOrRolledBackBeforeAllExecutions() {
+ const { executor } = this.#props
+
+ const originalExecuteQuery = executor.executeQuery.bind(executor)
+ executor.executeQuery = async (query, queryId) => {
+ this.#assertNotCommittedOrRolledBack()
+ return await originalExecuteQuery(query, queryId)
+ }
+
+ const that = this
+ const originalStream = executor.stream.bind(executor)
+ executor.stream = (query, chunkSize, queryId) => {
+ that.#assertNotCommittedOrRolledBack()
+ return originalStream(query, chunkSize, queryId)
+ }
+ }
+
+ #assertNotCommittedOrRolledBack(): void {
+ if (this.isCommitted) {
+ throw new Error('Transaction is already committed')
+ }
+
+ if (this.isRolledBack) {
+ throw new Error('Transaction is already rolled back')
+ }
+ }
+}
+
+interface ControlledTransactionProps extends KyselyProps {
+ readonly connection: ControlledConnection
+}
+
+export class Command {
+ readonly #cb: () => Promise
+
+ constructor(cb: () => Promise) {
+ this.#cb = cb
+ }
+
+ /**
+ * Executes the command.
+ */
+ async execute(): Promise {
+ return await this.#cb()
+ }
+}
diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts
index ec7a01971..129496292 100644
--- a/src/operation-node/operation-node-transformer.ts
+++ b/src/operation-node/operation-node-transformer.ts
@@ -94,6 +94,7 @@ import { CastNode } from './cast-node.js'
import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
+import { RefreshMaterializedViewNode } from './refresh-materialized-view-node.js'
/**
* Transforms an operation node tree into another one.
@@ -189,6 +190,8 @@ export class OperationNodeTransformer {
DropConstraintNode: this.transformDropConstraint.bind(this),
ForeignKeyConstraintNode: this.transformForeignKeyConstraint.bind(this),
CreateViewNode: this.transformCreateView.bind(this),
+ RefreshMaterializedViewNode:
+ this.transformRefreshMaterializedView.bind(this),
DropViewNode: this.transformDropView.bind(this),
GeneratedNode: this.transformGenerated.bind(this),
DefaultValueNode: this.transformDefaultValue.bind(this),
@@ -796,6 +799,17 @@ export class OperationNodeTransformer {
})
}
+ protected transformRefreshMaterializedView(
+ node: RefreshMaterializedViewNode,
+ ): RefreshMaterializedViewNode {
+ return requireAllProps({
+ kind: 'RefreshMaterializedViewNode',
+ name: this.transformNode(node.name),
+ concurrently: node.concurrently,
+ withNoData: node.withNoData,
+ })
+ }
+
protected transformDropView(node: DropViewNode): DropViewNode {
return requireAllProps({
kind: 'DropViewNode',
diff --git a/src/operation-node/operation-node-visitor.ts b/src/operation-node/operation-node-visitor.ts
index f88f12539..533f58098 100644
--- a/src/operation-node/operation-node-visitor.ts
+++ b/src/operation-node/operation-node-visitor.ts
@@ -96,6 +96,7 @@ import { CastNode } from './cast-node.js'
import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
+import { RefreshMaterializedViewNode } from './refresh-materialized-view-node.js'
export abstract class OperationNodeVisitor {
protected readonly nodeStack: OperationNode[] = []
@@ -166,6 +167,7 @@ export abstract class OperationNodeVisitor {
DropConstraintNode: this.visitDropConstraint.bind(this),
ForeignKeyConstraintNode: this.visitForeignKeyConstraint.bind(this),
CreateViewNode: this.visitCreateView.bind(this),
+ RefreshMaterializedViewNode: this.visitRefreshMaterializedView.bind(this),
DropViewNode: this.visitDropView.bind(this),
GeneratedNode: this.visitGenerated.bind(this),
DefaultValueNode: this.visitDefaultValue.bind(this),
@@ -277,6 +279,9 @@ export abstract class OperationNodeVisitor {
protected abstract visitPrimitiveValueList(node: PrimitiveValueListNode): void
protected abstract visitOperator(node: OperatorNode): void
protected abstract visitCreateView(node: CreateViewNode): void
+ protected abstract visitRefreshMaterializedView(
+ node: RefreshMaterializedViewNode,
+ ): void
protected abstract visitDropView(node: DropViewNode): void
protected abstract visitGenerated(node: GeneratedNode): void
protected abstract visitDefaultValue(node: DefaultValueNode): void
diff --git a/src/operation-node/operation-node.ts b/src/operation-node/operation-node.ts
index 6476a24eb..7786b7ec1 100644
--- a/src/operation-node/operation-node.ts
+++ b/src/operation-node/operation-node.ts
@@ -58,6 +58,7 @@ export type OperationNodeKind =
| 'AddConstraintNode'
| 'DropConstraintNode'
| 'CreateViewNode'
+ | 'RefreshMaterializedViewNode'
| 'DropViewNode'
| 'GeneratedNode'
| 'DefaultValueNode'
diff --git a/src/operation-node/refresh-materialized-view-node.ts b/src/operation-node/refresh-materialized-view-node.ts
new file mode 100644
index 000000000..e421a087b
--- /dev/null
+++ b/src/operation-node/refresh-materialized-view-node.ts
@@ -0,0 +1,41 @@
+import { freeze } from '../util/object-utils.js'
+import { OperationNode } from './operation-node.js'
+import { SchemableIdentifierNode } from './schemable-identifier-node.js'
+
+export type RefreshMaterializedViewNodeParams = Omit<
+ Partial,
+ 'kind' | 'name'
+>
+
+export interface RefreshMaterializedViewNode extends OperationNode {
+ readonly kind: 'RefreshMaterializedViewNode'
+ readonly name: SchemableIdentifierNode
+ readonly concurrently?: boolean
+ readonly withNoData?: boolean
+}
+
+/**
+ * @internal
+ */
+export const RefreshMaterializedViewNode = freeze({
+ is(node: OperationNode): node is RefreshMaterializedViewNode {
+ return node.kind === 'RefreshMaterializedViewNode'
+ },
+
+ create(name: string): RefreshMaterializedViewNode {
+ return freeze({
+ kind: 'RefreshMaterializedViewNode',
+ name: SchemableIdentifierNode.create(name),
+ })
+ },
+
+ cloneWith(
+ createView: RefreshMaterializedViewNode,
+ params: RefreshMaterializedViewNodeParams,
+ ): RefreshMaterializedViewNode {
+ return freeze({
+ ...createView,
+ ...params,
+ })
+ },
+})
diff --git a/src/operation-node/update-query-node.ts b/src/operation-node/update-query-node.ts
index c4188205a..a815a45c9 100644
--- a/src/operation-node/update-query-node.ts
+++ b/src/operation-node/update-query-node.ts
@@ -12,6 +12,7 @@ import { ExplainNode } from './explain-node.js'
import { LimitNode } from './limit-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
+import { ListNode } from './list-node.js'
export type UpdateValuesNode = ValueListNode | PrimitiveValueListNode
@@ -39,10 +40,15 @@ export const UpdateQueryNode = freeze({
return node.kind === 'UpdateQueryNode'
},
- create(table: OperationNode, withNode?: WithNode): UpdateQueryNode {
+ create(
+ tables: ReadonlyArray,
+ withNode?: WithNode,
+ ): UpdateQueryNode {
return freeze({
kind: 'UpdateQueryNode',
- table,
+ // For backwards compatibility, use the raw table node when there's only one table
+ // and don't rename the property to something like `tables`.
+ table: tables.length === 1 ? tables[0] : ListNode.create(tables),
...(withNode && { with: withNode }),
})
},
diff --git a/src/operation-node/value-node.ts b/src/operation-node/value-node.ts
index 2c811d0dd..b492fd46b 100644
--- a/src/operation-node/value-node.ts
+++ b/src/operation-node/value-node.ts
@@ -5,6 +5,7 @@ export interface ValueNode extends OperationNode {
readonly kind: 'ValueNode'
readonly value: unknown
readonly immediate?: boolean
+ readonly serialized?: boolean
}
/**
@@ -15,9 +16,10 @@ export const ValueNode = freeze({
return node.kind === 'ValueNode'
},
- create(value: unknown): ValueNode {
+ create(value: unknown, props?: { serialized?: boolean }): ValueNode {
return freeze({
kind: 'ValueNode',
+ ...props,
value,
})
},
diff --git a/src/parser/delete-from-parser.ts b/src/parser/delete-from-parser.ts
new file mode 100644
index 000000000..f46143277
--- /dev/null
+++ b/src/parser/delete-from-parser.ts
@@ -0,0 +1,30 @@
+import type { DeleteQueryBuilder } from '../query-builder/delete-query-builder.js'
+import type { DeleteResult } from '../query-builder/delete-result.js'
+import type { ShallowRecord } from '../util/type-utils.js'
+import type {
+ ExtractTableAlias,
+ From,
+ FromTables,
+ TableExpressionOrList,
+} from './table-parser.js'
+
+export type DeleteFrom> =
+ TE extends ReadonlyArray
+ ? DeleteQueryBuilder, FromTables, DeleteResult>
+ : TE extends keyof DB & string
+ ? // This branch creates a good-looking type for the most common case:
+ // deleteFrom('person') --> DeleteQueryBuilder.
+ // ExtractTableAlias is needed for the case where DB == any. Without it:
+ // deleteFrom('person as p') --> DeleteQueryBuilder
+ DeleteQueryBuilder, DeleteResult>
+ : // This branch creates a good-looking type for common aliased case:
+ // deleteFrom('person as p') --> DeleteQueryBuilder.
+ TE extends `${infer T} as ${infer A}`
+ ? T extends keyof DB
+ ? DeleteQueryBuilder, A, DeleteResult>
+ : never
+ : DeleteQueryBuilder<
+ From,
+ FromTables,
+ DeleteResult
+ >
diff --git a/src/parser/merge-into-parser.ts b/src/parser/merge-into-parser.ts
new file mode 100644
index 000000000..3a4df1fd3
--- /dev/null
+++ b/src/parser/merge-into-parser.ts
@@ -0,0 +1,15 @@
+import type { MergeQueryBuilder } from '../query-builder/merge-query-builder.js'
+import type { MergeResult } from '../query-builder/merge-result.js'
+import type { ShallowRecord } from '../util/type-utils.js'
+import type { ExtractTableAlias, SimpleTableReference } from './table-parser.js'
+
+export type MergeInto<
+ DB,
+ TE extends SimpleTableReference,
+> = TE extends keyof DB & string
+ ? MergeQueryBuilder, MergeResult>
+ : TE extends `${infer T} as ${infer A}`
+ ? T extends keyof DB
+ ? MergeQueryBuilder, A, MergeResult>
+ : never
+ : never
diff --git a/src/parser/savepoint-parser.ts b/src/parser/savepoint-parser.ts
new file mode 100644
index 000000000..73297aa1b
--- /dev/null
+++ b/src/parser/savepoint-parser.ts
@@ -0,0 +1,30 @@
+import { IdentifierNode } from '../operation-node/identifier-node.js'
+import { RawNode } from '../operation-node/raw-node.js'
+
+export type RollbackToSavepoint<
+ S extends string[],
+ SN extends S[number],
+> = S extends [...infer L, infer R]
+ ? R extends SN
+ ? S
+ : RollbackToSavepoint
+ : never
+
+export type ReleaseSavepoint<
+ S extends string[],
+ SN extends S[number],
+> = S extends [...infer L, infer R]
+ ? R extends SN
+ ? L
+ : ReleaseSavepoint
+ : never
+
+export function parseSavepointCommand(
+ command: string,
+ savepointName: string,
+): RawNode {
+ return RawNode.createWithChildren([
+ RawNode.createWithSql(`${command} `),
+ IdentifierNode.create(savepointName), // ensures savepointName gets sanitized
+ ])
+}
diff --git a/src/parser/select-from-parser.ts b/src/parser/select-from-parser.ts
new file mode 100644
index 000000000..521daafd5
--- /dev/null
+++ b/src/parser/select-from-parser.ts
@@ -0,0 +1,29 @@
+import type { SelectQueryBuilder } from '../query-builder/select-query-builder.js'
+import type { ShallowRecord } from '../util/type-utils.js'
+import type {
+ ExtractTableAlias,
+ From,
+ FromTables,
+ TableExpressionOrList,
+} from './table-parser.js'
+
+export type SelectFrom<
+ DB,
+ TB extends keyof DB,
+ TE extends TableExpressionOrList,
+> =
+ TE extends ReadonlyArray
+ ? SelectQueryBuilder, FromTables, {}>
+ : TE extends keyof DB & string
+ ? // This branch creates a good-looking type for the most common case:
+ // selectFrom('person') --> SelectQueryBuilder.
+ // ExtractTableAlias is needed for the case where DB == any. Without it:
+ // selectFrom('person as p') --> SelectQueryBuilder
+ SelectQueryBuilder, {}>
+ : // This branch creates a good-looking type for common aliased case:
+ // selectFrom('person as p') --> SelectQueryBuilder.
+ TE extends `${infer T} as ${infer A}`
+ ? T extends keyof DB
+ ? SelectQueryBuilder, TB | A, {}>
+ : never
+ : SelectQueryBuilder, FromTables, {}>
diff --git a/src/parser/table-parser.ts b/src/parser/table-parser.ts
index 9c665c16e..d9e9fed03 100644
--- a/src/parser/table-parser.ts
+++ b/src/parser/table-parser.ts
@@ -8,7 +8,7 @@ import {
import { IdentifierNode } from '../operation-node/identifier-node.js'
import { OperationNode } from '../operation-node/operation-node.js'
import { AliasedExpression } from '../expression/expression.js'
-import { DrainOuterGeneric, ShallowRecord } from '../util/type-utils.js'
+import { DrainOuterGeneric } from '../util/type-utils.js'
export type TableExpression =
| AnyAliasedTable
@@ -19,17 +19,9 @@ export type TableExpressionOrList =
| TableExpression
| ReadonlyArray>
-export type TableReference =
- | SimpleTableReference
- | AliasedExpression
-
export type SimpleTableReference = AnyAliasedTable | AnyTable
-
export type AnyAliasedTable = `${AnyTable} as ${string}`
-
-export type TableReferenceOrList =
- | TableReference
- | ReadonlyArray>
+export type AnyTable = keyof DB & string
export type From = DrainOuterGeneric<{
[C in
@@ -56,15 +48,6 @@ export type ExtractTableAlias = TE extends `${string} as ${infer TA}`
? TE
: never
-export type PickTableWithAlias<
- DB,
- T extends AnyAliasedTable,
-> = T extends `${infer TB} as ${infer A}`
- ? TB extends keyof DB
- ? ShallowRecord
- : never
- : never
-
type ExtractAliasFromTableExpression = TE extends string
? TE extends `${string} as ${infer TA}`
? TA
@@ -101,8 +84,6 @@ type ExtractRowTypeFromTableExpression<
: never
: never
-type AnyTable = keyof DB & string
-
export function parseTableExpressionOrList(
table: TableExpressionOrList,
): OperationNode[] {
diff --git a/src/parser/update-parser.ts b/src/parser/update-parser.ts
new file mode 100644
index 000000000..95f4a0863
--- /dev/null
+++ b/src/parser/update-parser.ts
@@ -0,0 +1,41 @@
+import type { UpdateQueryBuilder } from '../query-builder/update-query-builder.js'
+import type { UpdateResult } from '../query-builder/update-result.js'
+import type { ShallowRecord } from '../util/type-utils.js'
+import type {
+ ExtractTableAlias,
+ From,
+ FromTables,
+ TableExpressionOrList,
+} from './table-parser.js'
+
+export type UpdateTable> =
+ TE extends ReadonlyArray
+ ? UpdateQueryBuilder<
+ From,
+ FromTables,
+ FromTables,
+ UpdateResult
+ >
+ : TE extends keyof DB & string
+ ? // This branch creates a good-looking type for the most common case:
+ // updateTable('person') --> UpdateQueryBuilder.
+ // ExtractTableAlias is needed for the case where DB == any. Without it:
+ // updateTable('person as p') --> UpdateQueryBuilder
+ UpdateQueryBuilder<
+ DB,
+ ExtractTableAlias,
+ ExtractTableAlias,
+ UpdateResult
+ >
+ : // This branch creates a good-looking type for common aliased case:
+ // updateTable('person as p') --> UpdateQueryBuilder.
+ TE extends `${infer T} as ${infer A}`
+ ? T extends keyof DB
+ ? UpdateQueryBuilder, A, A, UpdateResult>
+ : never
+ : UpdateQueryBuilder<
+ From,
+ FromTables,
+ FromTables,
+ UpdateResult
+ >
diff --git a/src/parser/update-set-parser.ts b/src/parser/update-set-parser.ts
index c3ce3fe4c..248491956 100644
--- a/src/parser/update-set-parser.ts
+++ b/src/parser/update-set-parser.ts
@@ -12,12 +12,19 @@ import {
parseReferenceExpression,
ReferenceExpression,
} from './reference-parser.js'
+import { AnyColumn, DrainOuterGeneric } from '../util/type-utils.js'
-export type UpdateObject = {
- [C in UpdateKeys]?:
- | ValueExpression>
- | undefined
-}
+export type UpdateObject<
+ DB,
+ TB extends keyof DB,
+ UT extends keyof DB = TB,
+> = DrainOuterGeneric<{
+ [C in AnyColumn]?: {
+ [T in UT]: C extends keyof DB[T]
+ ? ValueExpression> | undefined
+ : never
+ }[UT]
+}>
export type UpdateObjectFactory<
DB,
diff --git a/src/plugin/with-schema/with-schema-transformer.ts b/src/plugin/with-schema/with-schema-transformer.ts
index af14b0b3d..3fd15a0e1 100644
--- a/src/plugin/with-schema/with-schema-transformer.ts
+++ b/src/plugin/with-schema/with-schema-transformer.ts
@@ -1,5 +1,6 @@
import { AliasNode } from '../../operation-node/alias-node.js'
import { IdentifierNode } from '../../operation-node/identifier-node.js'
+import { ListNode } from '../../operation-node/list-node.js'
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js'
import { OperationNode } from '../../operation-node/operation-node.js'
import { ReferencesNode } from '../../operation-node/references-node.js'
@@ -21,6 +22,7 @@ const ROOT_OPERATION_NODES: Record = freeze({
CreateTableNode: true,
CreateTypeNode: true,
CreateViewNode: true,
+ RefreshMaterializedViewNode: true,
DeleteQueryNode: true,
DropIndexNode: true,
DropSchemaNode: true,
@@ -157,14 +159,14 @@ export class WithSchemaTransformer extends OperationNodeTransformer {
node: OperationNode,
schemableIds: Set,
): void {
- const table = TableNode.is(node)
- ? node
- : AliasNode.is(node) && TableNode.is(node.node)
- ? node.node
- : null
-
- if (table) {
- this.#collectSchemableId(table.table, schemableIds)
+ if (TableNode.is(node)) {
+ this.#collectSchemableId(node.table, schemableIds)
+ } else if (AliasNode.is(node) && TableNode.is(node.node)) {
+ this.#collectSchemableId(node.node.table, schemableIds)
+ } else if (ListNode.is(node)) {
+ for (const table of node.items) {
+ this.#collectSchemableIdsFromTableExpr(table, schemableIds)
+ }
}
}
diff --git a/src/query-builder/aggregate-function-builder.ts b/src/query-builder/aggregate-function-builder.ts
index ebd559fbf..0b1f93095 100644
--- a/src/query-builder/aggregate-function-builder.ts
+++ b/src/query-builder/aggregate-function-builder.ts
@@ -2,7 +2,6 @@ import { freeze } from '../util/object-utils.js'
import { AggregateFunctionNode } from '../operation-node/aggregate-function-node.js'
import { AliasNode } from '../operation-node/alias-node.js'
import { IdentifierNode } from '../operation-node/identifier-node.js'
-import { preventAwait } from '../util/prevent-await.js'
import { OverBuilder } from './over-builder.js'
import { createOverBuilder } from '../parser/parse-utils.js'
import {
@@ -344,11 +343,6 @@ export class AggregateFunctionBuilder
}
}
-preventAwait(
- AggregateFunctionBuilder,
- "don't await AggregateFunctionBuilder instances. They are never executed directly and are always just a part of a query.",
-)
-
/**
* {@link AggregateFunctionBuilder} with an alias. The result of calling {@link AggregateFunctionBuilder.as}.
*/
diff --git a/src/query-builder/cte-builder.ts b/src/query-builder/cte-builder.ts
index b1d6013c3..7e0591640 100644
--- a/src/query-builder/cte-builder.ts
+++ b/src/query-builder/cte-builder.ts
@@ -1,6 +1,5 @@
import { OperationNodeSource } from '../operation-node/operation-node-source.js'
import { CommonTableExpressionNode } from '../operation-node/common-table-expression-node.js'
-import { preventAwait } from '../util/prevent-await.js'
import { freeze } from '../util/object-utils.js'
export class CTEBuilder implements OperationNodeSource {
@@ -39,11 +38,6 @@ export class CTEBuilder implements OperationNodeSource {
}
}
-preventAwait(
- CTEBuilder,
- "don't await CTEBuilder instances. They are never executed directly and are always just a part of a query.",
-)
-
interface CTEBuilderProps {
readonly node: CommonTableExpressionNode
}
diff --git a/src/query-builder/delete-query-builder.ts b/src/query-builder/delete-query-builder.ts
index 0dde970a9..c71ba9f65 100644
--- a/src/query-builder/delete-query-builder.ts
+++ b/src/query-builder/delete-query-builder.ts
@@ -35,7 +35,6 @@ import {
SimplifySingleResult,
SqlBool,
} from '../util/type-utils.js'
-import { preventAwait } from '../util/prevent-await.js'
import { Compilable } from '../util/compilable.js'
import { QueryExecutor } from '../query-executor/query-executor.js'
import { QueryId } from '../util/query-id.js'
@@ -1153,11 +1152,6 @@ export class DeleteQueryBuilder
}
}
-preventAwait(
- DeleteQueryBuilder,
- "don't await DeleteQueryBuilder instances directly. To execute the query you need to call `execute` or `executeTakeFirst`.",
-)
-
export interface DeleteQueryBuilderProps {
readonly queryId: QueryId
readonly queryNode: DeleteQueryNode
diff --git a/src/query-builder/insert-query-builder.ts b/src/query-builder/insert-query-builder.ts
index 2dc2b1483..2d6b481c1 100644
--- a/src/query-builder/insert-query-builder.ts
+++ b/src/query-builder/insert-query-builder.ts
@@ -22,7 +22,6 @@ import {
UpdateObjectExpression,
parseUpdateObjectExpression,
} from '../parser/update-set-parser.js'
-import { preventAwait } from '../util/prevent-await.js'
import { Compilable } from '../util/compilable.js'
import { QueryExecutor } from '../query-executor/query-executor.js'
import { QueryId } from '../util/query-id.js'
@@ -1187,11 +1186,6 @@ export class InsertQueryBuilder
}
}
-preventAwait(
- InsertQueryBuilder,
- "don't await InsertQueryBuilder instances directly. To execute the query you need to call `execute` or `executeTakeFirst`.",
-)
-
export interface InsertQueryBuilderProps {
readonly queryId: QueryId
readonly queryNode: InsertQueryNode
diff --git a/src/query-builder/join-builder.ts b/src/query-builder/join-builder.ts
index 9866faa67..da2163877 100644
--- a/src/query-builder/join-builder.ts
+++ b/src/query-builder/join-builder.ts
@@ -10,7 +10,6 @@ import {
import { ExpressionOrFactory } from '../parser/expression-parser.js'
import { ReferenceExpression } from '../parser/reference-parser.js'
import { freeze } from '../util/object-utils.js'
-import { preventAwait } from '../util/prevent-await.js'
import { SqlBool } from '../util/type-utils.js'
export class JoinBuilder
@@ -92,11 +91,6 @@ export class JoinBuilder
}
}
-preventAwait(
- JoinBuilder,
- "don't await JoinBuilder instances. They are never executed directly and are always just a part of a query.",
-)
-
export interface JoinBuilderProps {
readonly joinNode: JoinNode
}
diff --git a/src/query-builder/merge-query-builder.ts b/src/query-builder/merge-query-builder.ts
index d7c8fe20e..e4ea17841 100644
--- a/src/query-builder/merge-query-builder.ts
+++ b/src/query-builder/merge-query-builder.ts
@@ -37,7 +37,6 @@ import { NOOP_QUERY_EXECUTOR } from '../query-executor/noop-query-executor.js'
import { QueryExecutor } from '../query-executor/query-executor.js'
import { Compilable } from '../util/compilable.js'
import { freeze } from '../util/object-utils.js'
-import { preventAwait } from '../util/prevent-await.js'
import { QueryId } from '../util/query-id.js'
import {
ShallowRecord,
@@ -263,11 +262,6 @@ export class MergeQueryBuilder
}
}
-preventAwait(
- MergeQueryBuilder,
- "don't await MergeQueryBuilder instances directly. To execute the query you need to call `execute` when available.",
-)
-
export interface MergeQueryBuilderProps {
readonly queryId: QueryId
readonly queryNode: MergeQueryNode
@@ -840,11 +834,6 @@ export class WheneableMergeQueryBuilder<
}
}
-preventAwait(
- WheneableMergeQueryBuilder,
- "don't await WheneableMergeQueryBuilder instances directly. To execute the query you need to call `execute`.",
-)
-
export class MatchedThenableMergeQueryBuilder<
DB,
TT extends keyof DB,
@@ -1041,11 +1030,6 @@ export class MatchedThenableMergeQueryBuilder<
}
}
-preventAwait(
- MatchedThenableMergeQueryBuilder,
- "don't await MatchedThenableMergeQueryBuilder instances directly. To execute the query you need to call `execute` when available.",
-)
-
export class NotMatchedThenableMergeQueryBuilder<
DB,
TT extends keyof DB,
@@ -1152,11 +1136,6 @@ export class NotMatchedThenableMergeQueryBuilder<
}
}
-preventAwait(
- NotMatchedThenableMergeQueryBuilder,
- "don't await NotMatchedThenableMergeQueryBuilder instances directly. To execute the query you need to call `execute` when available.",
-)
-
export type ExtractWheneableMergeQueryBuilder<
DB,
TT extends keyof DB,
diff --git a/src/query-builder/on-conflict-builder.ts b/src/query-builder/on-conflict-builder.ts
index 6fce43e1c..fce275dc5 100644
--- a/src/query-builder/on-conflict-builder.ts
+++ b/src/query-builder/on-conflict-builder.ts
@@ -17,7 +17,6 @@ import {
} from '../parser/update-set-parser.js'
import { Updateable } from '../util/column-type.js'
import { freeze } from '../util/object-utils.js'
-import { preventAwait } from '../util/prevent-await.js'
import { AnyColumn, SqlBool } from '../util/type-utils.js'
import { WhereInterface } from './where-interface.js'
@@ -276,8 +275,6 @@ export interface OnConflictBuilderProps {
readonly onConflictNode: OnConflictNode
}
-preventAwait(OnConflictBuilder, "don't await OnConflictBuilder instances.")
-
export type OnConflictDatabase = {
[K in keyof DB | 'excluded']: Updateable
}
@@ -298,11 +295,6 @@ export class OnConflictDoNothingBuilder
}
}
-preventAwait(
- OnConflictDoNothingBuilder,
- "don't await OnConflictDoNothingBuilder instances.",
-)
-
export class OnConflictUpdateBuilder
implements WhereInterface, OperationNodeSource
{
@@ -383,8 +375,3 @@ export class OnConflictUpdateBuilder
return this.#props.onConflictNode
}
}
-
-preventAwait(
- OnConflictUpdateBuilder,
- "don't await OnConflictUpdateBuilder instances.",
-)
diff --git a/src/query-builder/over-builder.ts b/src/query-builder/over-builder.ts
index 330be843a..675f4950c 100644
--- a/src/query-builder/over-builder.ts
+++ b/src/query-builder/over-builder.ts
@@ -12,7 +12,6 @@ import {
} from '../parser/partition-by-parser.js'
import { StringReference } from '../parser/reference-parser.js'
import { freeze } from '../util/object-utils.js'
-import { preventAwait } from '../util/prevent-await.js'
export class OverBuilder
implements OperationNodeSource
@@ -107,11 +106,6 @@ export class OverBuilder
}
}
-preventAwait(
- OverBuilder,
- "don't await OverBuilder instances. They are never executed directly and are always just a part of a query.",
-)
-
export interface OverBuilderProps {
readonly overNode: OverNode
}
diff --git a/src/query-builder/select-query-builder.ts b/src/query-builder/select-query-builder.ts
index da098a5ad..3fa5c9a6d 100644
--- a/src/query-builder/select-query-builder.ts
+++ b/src/query-builder/select-query-builder.ts
@@ -40,7 +40,6 @@ import {
UndirectedOrderByExpression,
parseOrderBy,
} from '../parser/order-by-parser.js'
-import { preventAwait } from '../util/prevent-await.js'
import { LimitNode } from '../operation-node/limit-node.js'
import { OffsetNode } from '../operation-node/offset-node.js'
import { Compilable } from '../util/compilable.js'
@@ -243,7 +242,7 @@ export interface SelectQueryBuilder
* import { sql } from 'kysely'
*
* const persons = await db.selectFrom('person')
- * .select(({ eb, selectFrom, or }) => [
+ * .select(({ eb, selectFrom, or, val, lit }) => [
* // Select a correlated subquery
* selectFrom('pet')
* .whereRef('person.id', '=', 'pet.owner_id')
@@ -260,7 +259,13 @@ export interface SelectQueryBuilder
* ]).as('is_jennifer_or_arnold'),
*
* // Select a raw sql expression
- * sql`concat(first_name, ' ', last_name)`.as('full_name')
+ * sql`concat(first_name, ' ', last_name)`.as('full_name').
+ *
+ * // Select a static string value
+ * val('Some value').as('string_value'),
+ *
+ * // Select a literal value
+ * lit(42).as('literal_value'),
* ])
* .execute()
* ```
@@ -277,7 +282,9 @@ export interface SelectQueryBuilder
* limit $1
* ) as "pet_name",
* ("first_name" = $2 or "first_name" = $3) as "jennifer_or_arnold",
- * concat(first_name, ' ', last_name) as "full_name"
+ * concat(first_name, ' ', last_name) as "full_name",
+ * $4 as "string_value",
+ * 42 as "literal_value"
* from "person"
* ```
*
@@ -1928,6 +1935,54 @@ export interface SelectQueryBuilder
? ExpressionWrapper
: KyselyTypeError<'$asTuple() call failed: All selected columns must be provided as arguments'>
+ /**
+ * Plucks the value type of the output record.
+ *
+ * In SQL, any record type that only has one column can be used as a scalar.
+ * For example a query like this works:
+ *
+ * ```sql
+ * select
+ * id,
+ * first_name
+ * from
+ * person as p
+ * where
+ * -- This is ok since the query only selects one row
+ * -- and one column.
+ * (select name from pet where pet.owner_id = p.id limit 1) = 'Doggo'
+ * ```
+ *
+ * In many cases Kysely handles this automatically and picks the correct
+ * scalar type instead of the record type, but sometimes you need to give
+ * Kysely a hint.
+ *
+ * One such case are custom helper functions that take `Expression`
+ * instances as inputs:
+ *
+ * ```ts
+ * function doStuff(expr: Expression) {
+ * ...
+ * }
+ *
+ * // Error! This is not ok because the expression type is
+ * // `{ first_name: string }` instead of `string`.
+ * doStuff(db.selectFrom('person').select('first_name'))
+ *
+ * // Ok! This is ok since we've plucked the `string` type of the
+ * // only column in the output type.
+ * doStuff(db.selectFrom('person').select('first_name').$asScalar())
+ * ```
+ *
+ * This function has absolutely no effect on the generated SQL. It's
+ * purely a type-level helper.
+ *
+ * This method returns an `ExpressionWrapper` instead of a `SelectQueryBuilder`
+ * since the return value should only be used as a part of an expression
+ * and never executed as the main query.
+ */
+ $asScalar(): ExpressionWrapper
+
/**
* Narrows (parts of) the output type of the query.
*
@@ -2561,6 +2616,10 @@ class SelectQueryBuilderImpl
return new ExpressionWrapper(this.toOperationNode())
}
+ $asScalar(): ExpressionWrapper