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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
- name: Build
run: |
make generate
make validate
- uses: actions/upload-artifact@v7
with:
name: plugin
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 5 additions & 1 deletion examples/bun-mysql2/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Binary file modified examples/bun-pg/bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions examples/bun-pg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"module": "src/main.ts",
"type": "module",
"devDependencies": {
"@types/pg": "^8.10.9",
"bun-types": "latest"
},
"peerDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions examples/bun-pg/src/db/query_sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export async function getAuthor(client: Client, args: GetAuthorArgs): Promise<Ge
return null;
}
const row = result.rows[0];
if (!row) {
return null;
}
return {
id: row[0],
name: row[1],
Expand Down Expand Up @@ -91,6 +94,9 @@ export async function createAuthor(client: Client, args: CreateAuthorArgs): Prom
return null;
}
const row = result.rows[0];
if (!row) {
return null;
}
return {
id: row[0],
name: row[1],
Expand Down
6 changes: 5 additions & 1 deletion examples/node-better-sqlite3/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
6 changes: 5 additions & 1 deletion examples/node-mysql2/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
6 changes: 6 additions & 0 deletions examples/node-pg/src/db/query_sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export async function getAuthor(client: Client, args: GetAuthorArgs): Promise<Ge
return null;
}
const row = result.rows[0];
if (!row) {
return null;
}
return {
id: row[0],
name: row[1],
Expand Down Expand Up @@ -91,6 +94,9 @@ export async function createAuthor(client: Client, args: CreateAuthorArgs): Prom
return null;
}
const row = result.rows[0];
if (!row) {
return null;
}
return {
id: row[0],
name: row[1],
Expand Down
2 changes: 1 addition & 1 deletion examples/node-pg/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@

/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
Expand All @@ -96,7 +97,6 @@
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
Expand Down
30 changes: 30 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions src/drivers/pg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,17 @@ export class Driver {
NodeFlags.TypeExcludesFlags
)
),
factory.createIfStatement(
factory.createPrefixUnaryExpression(
SyntaxKind.ExclamationToken,
factory.createIdentifier("row")
),
factory.createBlock(
[factory.createReturnStatement(factory.createNull())],
true
),
undefined
),
factory.createReturnStatement(
factory.createObjectLiteralExpression(
columns.map((col, i) =>
Expand Down
99 changes: 99 additions & 0 deletions test/pg-one.test.mjs
Original file line number Diff line number Diff line change
@@ -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")
);
});