From 72a727b790a6869e1e6c03ec538d973c7ba7296e Mon Sep 17 00:00:00 2001 From: Oula Kuuva Date: Mon, 6 Jul 2026 14:45:06 +0300 Subject: [PATCH 1/3] fix(pg): guard :one row access --- examples/bun-pg/src/db/query_sql.ts | 6 ++++++ examples/node-pg/src/db/query_sql.ts | 6 ++++++ examples/node-pg/tsconfig.json | 2 +- src/drivers/pg.ts | 11 +++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/examples/bun-pg/src/db/query_sql.ts b/examples/bun-pg/src/db/query_sql.ts index 525d4a4..0b57676 100644 --- a/examples/bun-pg/src/db/query_sql.ts +++ b/examples/bun-pg/src/db/query_sql.ts @@ -30,6 +30,9 @@ export async function getAuthor(client: Client, args: GetAuthorArgs): Promise From 1d8d98be56c286cbe16d4625696a5d97dce5c8b0 Mon Sep 17 00:00:00 2001 From: Oula Kuuva Date: Mon, 6 Jul 2026 14:54:46 +0300 Subject: [PATCH 2/3] test(pg): validate guarded :one rows Compile generated pg outputs against real @types/pg under noUncheckedIndexedAccess. --- .github/workflows/ci.yml | 1 + Makefile | 5 +- package-lock.json | 30 ++++++++++++ package.json | 3 +- test/pg-one.test.mjs | 99 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 test/pg-one.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a007a43..d9c7d2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: - name: Build run: | make generate + make validate - uses: actions/upload-artifact@v7 with: name: plugin diff --git a/Makefile b/Makefile index 5bbe894..42f3684 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ -.PHONY: clean generate +.PHONY: clean generate validate generate: examples/plugin.wasm examples/sqlc.dev.yaml cd examples && sqlc -f sqlc.dev.yaml generate +validate: node_modules + npm test + # https://github.com/bytecodealliance/javy/releases/tag/v7.0.1 examples/plugin.wasm: out.js bin/javy ./bin/javy build out.js --codegen source=omitted -o examples/plugin.wasm diff --git a/package-lock.json b/package-lock.json index 46d3c81..c923afa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "pg": "^8.22.0" }, "devDependencies": { + "@types/pg": "^8.10.9", "esbuild": "^0.28.1", "typescript": "^6.0.3" } @@ -466,6 +467,28 @@ "node": ">=18" } }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -664,6 +687,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index dd26e6d..2bd505e 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,12 @@ "description": "", "main": "app.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "tsc --noEmit && node --test" }, "author": "", "license": "ISC", "devDependencies": { + "@types/pg": "^8.10.9", "esbuild": "^0.28.1", "typescript": "^6.0.3" }, diff --git a/test/pg-one.test.mjs b/test/pg-one.test.mjs new file mode 100644 index 0000000..ef41f62 --- /dev/null +++ b/test/pg-one.test.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import test from "node:test"; +import { dirname, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const generatedFiles = [ + "examples/node-pg/src/db/query_sql.ts", + "examples/bun-pg/src/db/query_sql.ts", +].map((path) => resolve(root, path)); +const tsc = resolve(root, "node_modules/.bin/tsc"); + +function relativePath(path) { + return relative(root, path); +} + +test("generated pg :one queries guard row access", () => { + let totalOneQueryCount = 0; + + for (const generatedFile of generatedFiles) { + assert.ok( + existsSync(generatedFile), + `Missing generated pg output: ${generatedFile}` + ); + + const source = readFileSync(generatedFile, "utf8"); + const oneQueryCount = [...source.matchAll(/-- name:\s+\w+\s+:one\b/g)] + .length; + + assert.ok( + oneQueryCount > 0, + `Expected at least one generated pg :one query to validate in ${relativePath(generatedFile)}.` + ); + + const guardedRowAccessCount = [ + ...source.matchAll( + /if \(result\.rows\.length !== 1\) \{\n\s+return null;\n\s+\}\n\s+const row = result\.rows\[0\];\n\s+if \(!row\) \{\n\s+return null;\n\s+\}/g + ), + ].length; + + assert.equal( + guardedRowAccessCount, + oneQueryCount, + `${relativePath(generatedFile)} must guard \`result.rows[0]\` after the exact row-count guard for every :one query.` + ); + + assert.doesNotMatch( + source, + /const row = result\.rows\[0\]!;/, + `${relativePath(generatedFile)} must not use non-null row assertions.` + ); + + totalOneQueryCount += oneQueryCount; + } + + assert.ok(totalOneQueryCount > 0, "Expected at least one pg :one query."); +}); + +test("generated pg output type-checks with strict noUncheckedIndexedAccess", () => { + assert.ok( + existsSync(tsc), + "Missing TypeScript compiler. Install project dependencies before running validation." + ); + + const result = spawnSync( + tsc, + [ + "--ignoreConfig", + "--strict", + "--noUncheckedIndexedAccess", + "--module", + "commonjs", + "--target", + "es2020", + "--esModuleInterop", + "--skipLibCheck", + "--noEmit", + ...generatedFiles, + ], + { cwd: root, encoding: "utf8" } + ); + + assert.ifError(result.error); + assert.equal( + result.status, + 0, + [ + "Generated pg output failed strict noUncheckedIndexedAccess type-check.", + result.stdout, + result.stderr, + ] + .filter(Boolean) + .join("\n") + ); +}); From e46f8757c6879e4da7be309310cae80ce90bb9fe Mon Sep 17 00:00:00 2001 From: Oula Kuuva Date: Tue, 7 Jul 2026 09:21:48 +0300 Subject: [PATCH 3/3] fix: guard indexed access in strict examples --- examples/bun-mysql2/src/main.ts | 6 +++++- examples/bun-pg/bun.lockb | Bin 7450 -> 8766 bytes examples/bun-pg/package.json | 1 + examples/node-better-sqlite3/src/main.ts | 6 +++++- examples/node-mysql2/src/main.ts | 6 +++++- src/app.ts | 2 +- 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/examples/bun-mysql2/src/main.ts b/examples/bun-mysql2/src/main.ts index c3dd59a..de34756 100644 --- a/examples/bun-mysql2/src/main.ts +++ b/examples/bun-mysql2/src/main.ts @@ -33,7 +33,11 @@ async function main() { console.log(authors); // Get that author - const seal = await getAuthor(client, { id: authors[0].id }); + const author = authors[0]; + if (!author) { + throw new Error("author not found"); + } + const seal = await getAuthor(client, { id: author.id }); if (seal === null) { throw new Error("seal not found"); } diff --git a/examples/bun-pg/bun.lockb b/examples/bun-pg/bun.lockb index ba155ae960764aeb4453c5763eddc0d98ba1064a..6bb8c821cc6c66c0de0e209511a4089addf400bc 100755 GIT binary patch delta 2309 zcmbVOdr(wm6#wq-g?+%XyRvXu7gJFYv0Zi-VOJz2B`gz6bMjR*3Ph{0?7-UK3ovyc zm1O6nrH)jZ57ZbO#~LF=%12rn<)f^r39~UDqvTi(lVRuFy%%@?X{OH1x95Jp^ZZ`t z?3S#fxlO57C1Y%Ys`ccd_fpD>Hr+`cex>Vp)0sbK&oQ2AZ2a|u-bH(RO|3&w2`N4> zYw)n!@xcjW3l@8=zEZ{pAfRQej=qV`1OkI=Z*q(o5v=x?c#9GI5$8IbcOj2K{)xnW zrB;vE?`9pS6D2ya`YI~Q*gl+_aNdlZ>;e+kAjeUMOyp@1LAy%i(TML%>~N^EPHXtL zN7Ow!dQHg!7{WD}+jUz<&HC)hq(Iu-#^ghfjn!0SpS&@BOFIV#RFk2HQyWvzUpCsK z8<7;SQt2{N=2k@25{em$q=W<9NU}5hg!750fy)sI>Ug&?31?$MKn5!nu88Ldg%!$l zIL4{TShE%pH|kNaw0`tHqEpB^49!z8=0uc-ZcczEWs0#Hkm@{|w{_DGFEQm3mz6iMpFTR43N)zQ01 zh+_0?KA2N~^co3x82uO`G}4$D*{D36T)l?QBAJs*vh9P88f5r@mVsT>CdY5{cWoKTSx=BP>3hoU3l zq6pAvNYttkpj;DXrGmqvs8B1wAZ?gP10F3|#Y72kHe3`1<|q;!MbV0=Fe@FL7De7@ z0j$wsB0VgLCaXtu0{jpzGQucvUFT5LsLP>4lb1|;DHC5d>BQI_Y>-2LVe0jd{oilp zm(S>|9F(*_Z*(Kkym#}K#yy6Mm-nBG+z|Wf=yMPD z=$klb;8XBpmJ)Je>Ic|`-~w_buFZxQV#f^Zdk`cNQZ7D8d|ZImSZ4uMctS`-6_4&V z)x}60DjZns#dkojNRPvS18cq52dZg@RCW4fGn323TObDb+%s)Ene#ujUHfHiz9dmTBN; zQ|fqe>aeBvcPzO_7`Va=o84w-Ol~vXzn^BS0*n4oaL%BBHG?D0%(e_u^Avh+rR>i1Dbs zu6Ad9!BxlfeJj~-8VU=A9Iq_hJu#AU|36|mg z+G$oavAS(e+n0?(%bK`{xSGT3|$`7I+~9B6DLDvN*eLD~1KK zl|O1QnQVS9Y$`k;F<_=q5*~cO@P>(zX`+b_5Fp`!5J(7+U?SDucYBKVjTcVy%=eys1~U{uLp~U5WDteqS?Jdu<#n2o>@FGD}^wH$nVdkGrIv!!Zu(}K>MMGmCo*s z4rFq1k-;7xaM;(=_47q@;?Q*-XZ z-0NYPCMm0V3@$keCl9obCbd@cIR32zf@*{#G_9>Le}&bi zQ2Z5cMh!PMFzN!L(GA(;#9Re54wNtOKlE#$Z>R`QQT-9n7LGt`wGO=jE>3Pbgq3Lz3IY-OngGYyotK#59-bPn+u%q>9S@1Po^ z`W>Lfq?{&7-&Hzk%psg=jfxBhTn}Hw1)!Zk-9R{wbCf>L!-97Z)ev5+aXNl0XR&6l z8VKX1Zh0n68$7KOaB{Ld*G#H1t`ObkET!okk4;&9Yw=)(KmmIsz2tpM3VOk7(WE!| zf2xB1bf{lIz3MBY+;q&xUgJKC?v_(kw5pn^ShYo8RF`?_blah>fl3)=l#gZ%_WH!I z=t((cP`96{gx{h$f0IBKOQpS?qa*#XPp!5^xU#}O#l^SB&pod;g=%S&)v}I17w!b^%qj?feB3$wC3BZx z8i)X70R$66@U3!~jp`vzaF`c$hM1NlL%{@Z;-K6d+?ZSeC&CDE0&@aRR7%PZd|3sY zucMw&qs7ysG$9tqp7z+v*h)Cl=}@DWLpm!-_d~W!k)TiqQ8CMk8w?oHy6{C#^J3^_s;%y@`g}Mn-H_K zb75-f604ZMSLxz~4`fDjeS?Fs(e!)or$;CmUXDU(Pq?Mw91f2=$sdik|19_W3scJP tuo~Ik&VBL3l4i<=Y$G?8O^^1E3}kbd+_a^>{`|JcK^Hv?Z=`zj#J@=!6=47X diff --git a/examples/bun-pg/package.json b/examples/bun-pg/package.json index c264672..4906371 100644 --- a/examples/bun-pg/package.json +++ b/examples/bun-pg/package.json @@ -3,6 +3,7 @@ "module": "src/main.ts", "type": "module", "devDependencies": { + "@types/pg": "^8.10.9", "bun-types": "latest" }, "peerDependencies": { diff --git a/examples/node-better-sqlite3/src/main.ts b/examples/node-better-sqlite3/src/main.ts index 5ee0ee4..8ef814c 100644 --- a/examples/node-better-sqlite3/src/main.ts +++ b/examples/node-better-sqlite3/src/main.ts @@ -33,7 +33,11 @@ async function main() { console.log(authors); // Get that author - const seal = await getAuthor(database, { id: authors[0].id }); + const author = authors[0]; + if (!author) { + throw new Error("author not found"); + } + const seal = await getAuthor(database, { id: author.id }); if (seal === null) { throw new Error("seal not found"); } diff --git a/examples/node-mysql2/src/main.ts b/examples/node-mysql2/src/main.ts index d8576e3..4c1fa96 100644 --- a/examples/node-mysql2/src/main.ts +++ b/examples/node-mysql2/src/main.ts @@ -41,7 +41,11 @@ async function main() { console.log(authors); // Get that author - const seal = await getAuthor(client, { id: authors[0].id }); + const author = authors[0]; + if (!author) { + throw new Error("author not found"); + } + const seal = await getAuthor(client, { id: author.id }); if (seal === null) { throw new Error("seal not found"); } diff --git a/src/app.ts b/src/app.ts index 8c0a74f..d3ddcc5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -138,7 +138,7 @@ function codegen(input: GenerateRequest): GenerateResponse { colmap.set(column.name, count + 1); } - const lowerName = query.name[0].toLowerCase() + query.name.slice(1); + const lowerName = query.name.charAt(0).toLowerCase() + query.name.slice(1); const textName = `${lowerName}Query`; nodes.push(