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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bun.lock

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

20 changes: 20 additions & 0 deletions examples/native-sql/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@repo/example-native-sql",
"private": true,
"type": "module",
"scripts": {
"build": "bun codegen && bun build src/index.ts --outdir dist --target bun",
"codegen": "bun-sqlgen generate 'src/**/*.ts' --migrations src/db/migrations --package @repo/bun-sqlgen --no-schema",
"codegen:check": "bun-sqlgen generate 'src/**/*.ts' --migrations src/db/migrations --package @repo/bun-sqlgen --no-schema --check",
"check:types": "bun codegen:check && bun run --bun tsc"
},
"devDependencies": {
"@repo/bun-sqlgen": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/bun": "catalog:",
"typescript": "catalog:"
},
"imports": {
"#*": "./src/*"
}
}
5 changes: 5 additions & 0 deletions examples/native-sql/src/db/migrations/0001_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE messages (
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
body text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
10 changes: 10 additions & 0 deletions examples/native-sql/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Queries } from '#queries.gen.ts';
import { sql } from 'bun';

const messages = await sql<Queries['ListMessages'][]>`SELECT id, body, created_at FROM messages ORDER BY created_at DESC`;

console.log(messages[0]?.body, messages[0]?.created_at.toISOString());

const counts = await sql<Array<Queries['CountMessages']>>`SELECT count(*) AS total FROM messages`;

console.log(counts[0]?.total);
26 changes: 26 additions & 0 deletions examples/native-sql/src/queries.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// @generated by bun-sqlgen — do not edit.
// Run the generator to refresh.

/** Result of query `ListMessages`. */
export interface IListMessagesResult {
id: string;
body: string;
created_at: Date;
}

/** Result of query `CountMessages`. */
export interface ICountMessagesResult {
total: string | null;
}

export interface Queries {
ListMessages: IListMessagesResult;
CountMessages: ICountMessagesResult;
}

declare module "@repo/bun-sqlgen" {
interface QueryResults extends Queries {
}
}

export {};
8 changes: 8 additions & 0 deletions examples/native-sql/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
55 changes: 50 additions & 5 deletions packages/bun-sqlgen-core/src/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ function placeholderFor(dialect: Dialect): (n: number) => string {
* tag's type is Bun's `SQL` (or our `withTypes` wrapper, an intersection over it)
* — so aliases/re-exports/`Bun.sql`/wrapped clients all resolve.
*
* A query is a named tag — `sql.MyQuery\`...\`` — taking its name from the property.
* A bare `sql\`...\`` is a composable fragment, not a query.
* A query is either a named tag — `sql.MyQuery\`...\`` — or a bare tag whose
* result type indexes the generated registry — `sql<Queries['MyQuery'][]>\`...\``.
* An untyped bare `sql\`...\`` is a composable fragment, not a query.
*/
const COMPILER_OPTIONS: ts.CompilerOptions = {
target: ts.ScriptTarget.ES2022,
Expand Down Expand Up @@ -109,12 +110,13 @@ function discover(input: {
// see the wrapper type.
const bindings = collectTypedSqlBindings({ sf, checker });

// A query is a named tag, `sql.Name\`...\``; its name is the property. A bare
// `sql\`...\`` (no property) is a composable fragment, not a query.
// A query is a named tag, `sql.Name\`...\``, or a bare tag explicitly typed
// from the generated registry, `sql<Queries['Name'][]>\`...\``. Other bare tags
// stay fragments, including Bun's handwritten `sql<CustomRows[]>` escape hatch.
const found: Array<{ node: ts.TaggedTemplateExpression; name: string }> = [];
(function scan(node: ts.Node): void {
if (ts.isTaggedTemplateExpression(node)) {
const name = namedTag({ node, checker, bindings });
const name = namedTag({ node, checker, bindings }) ?? registryTypedTag({ node, checker });
if (name) {
found.push({ node, name });
}
Expand Down Expand Up @@ -150,6 +152,49 @@ interface TypedSqlBindings {
fields: Set<string>;
}

// `sql<Queries['MyQuery'][]>\`...\`` or
// `sql<Array<Queries['MyQuery']>>\`...\``: Bun's generic is the complete resolved
// value, so the registry member (a row) must be wrapped in an array. The literal
// key is also the query name, which keeps first-run generation independent of the
// generated module resolving successfully.
function registryTypedTag(input: {
node: ts.TaggedTemplateExpression;
checker: ts.TypeChecker;
}): string | null {
const { node, checker } = input;
if (!isBunSqlType({ expr: node.tag, checker }) || node.typeArguments?.length !== 1) {
return null;
}
const resultType = node.typeArguments[0]!;
const rowType = ts.isArrayTypeNode(resultType)
? resultType.elementType
: arrayElementType(resultType);
if (!rowType || !ts.isIndexedAccessTypeNode(rowType)) {
return null;
}
if (
!ts.isTypeReferenceNode(rowType.objectType) ||
!ts.isIdentifier(rowType.objectType.typeName) ||
rowType.objectType.typeName.text !== 'Queries'
) {
return null;
}
const key = rowType.indexType;
return ts.isLiteralTypeNode(key) && ts.isStringLiteralLike(key.literal) ? key.literal.text : null;
}

function arrayElementType(type: ts.TypeNode): ts.TypeNode | null {
if (
!ts.isTypeReferenceNode(type) ||
!ts.isIdentifier(type.typeName) ||
type.typeName.text !== 'Array' ||
type.typeArguments?.length !== 1
) {
return null;
}
return type.typeArguments[0]!;
}

// `sql.MyQuery\`...\``: a property-access tag whose object is a typed-SQL client and
// whose property isn't a real Bun `SQL` member (so `sql.begin\`...\`` is left alone).
function namedTag(input: {
Expand Down