From 94f06a7b7e3e87eed76b37a2cb9c467bf13d2b7c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 21 Jun 2025 11:04:04 +0200 Subject: [PATCH 1/7] basic inference of top level literals --- packages/cli/src/generator.ts | 8 +- packages/cli/src/parseRescript.ts | 2 - .../example/src/books/BookService__sql.res | 88 +-- packages/example/src/books/Misc.res | 64 +++ packages/example/src/books/Misc__sql.gen.tsx | 42 ++ packages/example/src/books/Misc__sql.res | 518 ++++++++++++++++++ packages/query/src/actions.ts | 95 +++- packages/runtime/src/preprocessor-sql.ts | 2 + packages/runtime/src/preprocessor-ts.ts | 1 + packages/runtime/src/preprocessor.ts | 1 + 10 files changed, 758 insertions(+), 63 deletions(-) create mode 100644 packages/example/src/books/Misc.res create mode 100644 packages/example/src/books/Misc__sql.gen.tsx create mode 100644 packages/example/src/books/Misc__sql.res diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index 99da7b68..46d83d99 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -83,7 +83,7 @@ export async function queryToTypeDeclarations( queryData = processTSQueryAST(parsedQuery.ast); } else { queryName = pascalCase(parsedQuery.ast.name); - queryData = processSQLQueryIR(queryASTToIR(parsedQuery.ast)); + queryData = processSQLQueryIR(queryASTToIR(parsedQuery.ast), queryName); } const typeData = await typeSource(queryData); @@ -100,7 +100,7 @@ export async function queryToTypeDeclarations( if (typeError || hasAnonymousColumns) { // tslint:disable:no-console if (typeError) { - console.error('Error in query. Details: %o', typeData); + console.error(`Error in query "${queryName}". Details: %o`, typeData); if (config.failOnError) { throw new Error( `Query "${queryName}" is invalid. Can't generate types.`, @@ -147,8 +147,6 @@ export async function queryToTypeDeclarations( return `#"${v.value}"`; case 'integer': return `#${v.value}`; - case 'float': - return `#${v.value}`; } }) .join(' | ')}]`; @@ -396,7 +394,7 @@ export async function generateDeclarationFile( `/**\n` + ` Runnable query:\n` + ` \`\`\`sql\n` + - `${processSQLQueryIR(typeDec.query.ir).query}\n` + + `${processSQLQueryIR(typeDec.query.ir, typeDec.query.name).query}\n` + ` \`\`\`\n\n` + ` */\n`; declarationFileContents += `@gentype diff --git a/packages/cli/src/parseRescript.ts b/packages/cli/src/parseRescript.ts index 828dc718..04fadc86 100644 --- a/packages/cli/src/parseRescript.ts +++ b/packages/cli/src/parseRescript.ts @@ -28,8 +28,6 @@ export function parseCode( .toString(), ); - content.reverse(); - const queries: Array = []; let unnamedQueriesCount = 0; diff --git a/packages/example/src/books/BookService__sql.res b/packages/example/src/books/BookService__sql.res index c142099a..92c2e916 100644 --- a/packages/example/src/books/BookService__sql.res +++ b/packages/example/src/books/BookService__sql.res @@ -8,15 +8,15 @@ type category = [#"novel" | #"science-fiction" | #"thriller"] @gentype type categoryArray = array -/** 'BooksByAuthor' parameters type */ +/** 'FindBookById' parameters type */ @gentype -type booksByAuthorParams = { - authorName: string, +type findBookByIdParams = { + id?: int, } -/** 'BooksByAuthor' return type */ +/** 'FindBookById' return type */ @gentype -type booksByAuthorResult = { +type findBookByIdResult = { author_id: option, categories: option, id: int, @@ -24,47 +24,45 @@ type booksByAuthorResult = { rank: option, } -/** 'BooksByAuthor' query type */ +/** 'FindBookById' query type */ @gentype -type booksByAuthorQuery = { - params: booksByAuthorParams, - result: booksByAuthorResult, +type findBookByIdQuery = { + params: findBookByIdParams, + result: findBookByIdResult, } -%%private(let booksByAuthorIR: IR.t = %raw(`{"usedParamSet":{"authorName":true},"params":[{"name":"authorName","required":true,"transform":{"type":"scalar"},"locs":[{"a":118,"b":129}]}],"statement":"SELECT b.* FROM books b\n INNER JOIN authors a ON a.id = b.author_id\n WHERE a.first_name || ' ' || a.last_name = :authorName!"}`)) +%%private(let findBookByIdIR: IR.t = %raw(`{"usedParamSet":{"id":true},"params":[{"name":"id","required":false,"transform":{"type":"scalar"},"locs":[{"a":31,"b":33}]}],"statement":"SELECT * FROM books WHERE id = :id"}`)) /** Runnable query: ```sql -SELECT b.* FROM books b - INNER JOIN authors a ON a.id = b.author_id - WHERE a.first_name || ' ' || a.last_name = $1 +SELECT * FROM books WHERE id = $1 ``` */ @gentype -module BooksByAuthor: { +module FindBookById: { /** Returns an array of all matched results. */ @gentype - let many: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise> + let many: (PgTyped.Pg.Client.t, findBookByIdParams) => promise> /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ @gentype - let one: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise> + let one: (PgTyped.Pg.Client.t, findBookByIdParams) => promise> /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ @gentype let expectOne: ( PgTyped.Pg.Client.t, - booksByAuthorParams, + findBookByIdParams, ~errorMessage: string=? - ) => promise + ) => promise /** Executes the query, but ignores whatever is returned by it. */ @gentype - let execute: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise + let execute: (PgTyped.Pg.Client.t, findBookByIdParams) => promise } = { - @module("pgtyped-rescript-runtime") @new external booksByAuthor: IR.t => PreparedStatement.t = "PreparedQuery"; - let query = booksByAuthor(booksByAuthorIR) + @module("pgtyped-rescript-runtime") @new external findBookById: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = findBookById(findBookByIdIR) let query = (params, ~client) => query->PreparedStatement.run(params, ~client) @gentype @@ -89,19 +87,19 @@ module BooksByAuthor: { } @gentype -@deprecated("Use 'BooksByAuthor.many' directly instead") -let booksByAuthor = (params, ~client) => BooksByAuthor.many(client, params) +@deprecated("Use 'FindBookById.many' directly instead") +let findBookById = (params, ~client) => FindBookById.many(client, params) -/** 'FindBookById' parameters type */ +/** 'BooksByAuthor' parameters type */ @gentype -type findBookByIdParams = { - id?: int, +type booksByAuthorParams = { + authorName: string, } -/** 'FindBookById' return type */ +/** 'BooksByAuthor' return type */ @gentype -type findBookByIdResult = { +type booksByAuthorResult = { author_id: option, categories: option, id: int, @@ -109,45 +107,47 @@ type findBookByIdResult = { rank: option, } -/** 'FindBookById' query type */ +/** 'BooksByAuthor' query type */ @gentype -type findBookByIdQuery = { - params: findBookByIdParams, - result: findBookByIdResult, +type booksByAuthorQuery = { + params: booksByAuthorParams, + result: booksByAuthorResult, } -%%private(let findBookByIdIR: IR.t = %raw(`{"usedParamSet":{"id":true},"params":[{"name":"id","required":false,"transform":{"type":"scalar"},"locs":[{"a":31,"b":33}]}],"statement":"SELECT * FROM books WHERE id = :id"}`)) +%%private(let booksByAuthorIR: IR.t = %raw(`{"usedParamSet":{"authorName":true},"params":[{"name":"authorName","required":true,"transform":{"type":"scalar"},"locs":[{"a":118,"b":129}]}],"statement":"SELECT b.* FROM books b\n INNER JOIN authors a ON a.id = b.author_id\n WHERE a.first_name || ' ' || a.last_name = :authorName!"}`)) /** Runnable query: ```sql -SELECT * FROM books WHERE id = $1 +SELECT b.* FROM books b + INNER JOIN authors a ON a.id = b.author_id + WHERE a.first_name || ' ' || a.last_name = $1 ``` */ @gentype -module FindBookById: { +module BooksByAuthor: { /** Returns an array of all matched results. */ @gentype - let many: (PgTyped.Pg.Client.t, findBookByIdParams) => promise> + let many: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise> /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ @gentype - let one: (PgTyped.Pg.Client.t, findBookByIdParams) => promise> + let one: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise> /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ @gentype let expectOne: ( PgTyped.Pg.Client.t, - findBookByIdParams, + booksByAuthorParams, ~errorMessage: string=? - ) => promise + ) => promise /** Executes the query, but ignores whatever is returned by it. */ @gentype - let execute: (PgTyped.Pg.Client.t, findBookByIdParams) => promise + let execute: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise } = { - @module("pgtyped-rescript-runtime") @new external findBookById: IR.t => PreparedStatement.t = "PreparedQuery"; - let query = findBookById(findBookByIdIR) + @module("pgtyped-rescript-runtime") @new external booksByAuthor: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = booksByAuthor(booksByAuthorIR) let query = (params, ~client) => query->PreparedStatement.run(params, ~client) @gentype @@ -172,7 +172,7 @@ module FindBookById: { } @gentype -@deprecated("Use 'FindBookById.many' directly instead") -let findBookById = (params, ~client) => FindBookById.many(client, params) +@deprecated("Use 'BooksByAuthor.many' directly instead") +let booksByAuthor = (params, ~client) => BooksByAuthor.many(client, params) diff --git a/packages/example/src/books/Misc.res b/packages/example/src/books/Misc.res new file mode 100644 index 00000000..b84c2f3c --- /dev/null +++ b/packages/example/src/books/Misc.res @@ -0,0 +1,64 @@ +let query = %sql.one(` + /* @name Literals */ + select + 'literal' as test_string_literal, + 1 as test_integer_literal, + 'hello ' || 'world' as test_regular_string +`) + +let moreExamples = %sql.one(` + /* @name MoreLiterals */ + select + 'success' as status, + 'error' as error_status, + 'pending' as pending_status, + 42 as magic_number, + 0 as zero_value, + -1 as negative_one, + 100 as max_percentage, + 'admin' as admin_role, + 'user' as user_role, + 'guest' as guest_role +`) + +// Test case with subquery - same alias appears multiple times +// This should test that duplicates are handled properly (removed from inference) +let duplicateAliasTest = %sql.one(` + /* @name DuplicateAliasTest */ + select + 'main' as status, + 1 as priority, + (select 'sub' as status) as sub_status +`) + +// Test with UNION - another case where same alias might appear +// Same aliases should prevent inference +let unionTest = %sql.many(` + /* @name UnionTest */ + select 'draft' as document_status, 1 as version + union all + select 'published' as document_status, 2 as version +`) + +// Test with single literals that should work +let singleLiterals = %sql.one(` + /* @name SingleLiterals */ + select + 'confirmed' as booking_status, + 5 as rating_stars, + 'premium' as subscription_tier +`) + +// Test edge cases for literal inference +let edgeCases = %sql.one(` + /* @name EdgeCases */ + select + '' as empty_string, + -999 as negative_number, + 0 as zero, + 'null' as null_string, + 123456789 as large_number, + 'a' as single_char, + 'with spaces' as string_with_spaces, + -1 as minus_one +`) diff --git a/packages/example/src/books/Misc__sql.gen.tsx b/packages/example/src/books/Misc__sql.gen.tsx new file mode 100644 index 00000000..3b38793e --- /dev/null +++ b/packages/example/src/books/Misc__sql.gen.tsx @@ -0,0 +1,42 @@ +/* TypeScript file generated from Misc__sql.res by genType. */ + +/* eslint-disable */ +/* tslint:disable */ + +const Misc__sqlJS = require('./Misc__sql.js'); + +import type {Pg_Client_t as PgTyped_Pg_Client_t} from 'pgtyped-rescript/src/res/PgTyped.gen'; + +/** 'Literals' parameters type */ +export type literalsParams = void; + +/** 'Literals' return type */ +export type literalsResult = { readonly integer_literal: (undefined | number); readonly literal: (undefined | string) }; + +/** 'Literals' query type */ +export type literalsQuery = { readonly params: literalsParams; readonly result: literalsResult }; + +/** Returns an array of all matched results. */ +export const Literals_many: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Promise = Misc__sqlJS.Literals.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const Literals_one: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Promise<(undefined | literalsResult)> = Misc__sqlJS.Literals.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const Literals_expectOne: (_1:PgTyped_Pg_Client_t, _2:literalsParams, errorMessage:(undefined | string)) => Promise = Misc__sqlJS.Literals.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const Literals_execute: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Promise = Misc__sqlJS.Literals.execute as any; + +export const literals: (params:literalsParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.literals as any; + +export const Literals: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:literalsParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Promise<(undefined | literalsResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Promise +} = Misc__sqlJS.Literals as any; diff --git a/packages/example/src/books/Misc__sql.res b/packages/example/src/books/Misc__sql.res new file mode 100644 index 00000000..3141067e --- /dev/null +++ b/packages/example/src/books/Misc__sql.res @@ -0,0 +1,518 @@ +/** Types generated for queries found in "src/books/Misc.res" */ +open PgTyped + + +/** 'Literals' parameters type */ +@gentype +type literalsParams = unit + +/** 'Literals' return type */ +@gentype +type literalsResult = { + test_integer_literal: [#1], + test_regular_string: option, + test_string_literal: [#"literal"], +} + +/** 'Literals' query type */ +@gentype +type literalsQuery = { + params: literalsParams, + result: literalsResult, +} + +%%private(let literalsIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n 'literal' as test_string_literal,\n 1 as test_integer_literal,\n 'hello ' || 'world' as test_regular_string"}`)) + +/** + Runnable query: + ```sql +select + 'literal' as test_string_literal, + 1 as test_integer_literal, + 'hello ' || 'world' as test_regular_string + ``` + + */ +@gentype +module Literals: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, literalsParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, literalsParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + literalsParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, literalsParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external literals: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = literals(literalsIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'Literals.many' directly instead") +let literals = (params, ~client) => Literals.many(client, params) + + +/** 'MoreLiterals' parameters type */ +@gentype +type moreLiteralsParams = unit + +/** 'MoreLiterals' return type */ +@gentype +type moreLiteralsResult = { + admin_role: [#"admin"], + error_status: [#"error"], + guest_role: [#"guest"], + magic_number: [#42], + max_percentage: [#100], + negative_one: option, + pending_status: [#"pending"], + status: [#"success"], + user_role: [#"user"], + zero_value: [#0], +} + +/** 'MoreLiterals' query type */ +@gentype +type moreLiteralsQuery = { + params: moreLiteralsParams, + result: moreLiteralsResult, +} + +%%private(let moreLiteralsIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n 'success' as status,\n 'error' as error_status,\n 'pending' as pending_status,\n 42 as magic_number,\n 0 as zero_value,\n -1 as negative_one,\n 100 as max_percentage,\n 'admin' as admin_role,\n 'user' as user_role,\n 'guest' as guest_role"}`)) + +/** + Runnable query: + ```sql +select + 'success' as status, + 'error' as error_status, + 'pending' as pending_status, + 42 as magic_number, + 0 as zero_value, + -1 as negative_one, + 100 as max_percentage, + 'admin' as admin_role, + 'user' as user_role, + 'guest' as guest_role + ``` + + */ +@gentype +module MoreLiterals: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, moreLiteralsParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, moreLiteralsParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + moreLiteralsParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, moreLiteralsParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external moreLiterals: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = moreLiterals(moreLiteralsIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'MoreLiterals.many' directly instead") +let moreLiterals = (params, ~client) => MoreLiterals.many(client, params) + + +/** 'DuplicateAliasTest' parameters type */ +@gentype +type duplicateAliasTestParams = unit + +/** 'DuplicateAliasTest' return type */ +@gentype +type duplicateAliasTestResult = { + priority: [#1], + status: option, + sub_status: option, +} + +/** 'DuplicateAliasTest' query type */ +@gentype +type duplicateAliasTestQuery = { + params: duplicateAliasTestParams, + result: duplicateAliasTestResult, +} + +%%private(let duplicateAliasTestIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n 'main' as status,\n 1 as priority,\n (select 'sub' as status) as sub_status"}`)) + +/** + Runnable query: + ```sql +select + 'main' as status, + 1 as priority, + (select 'sub' as status) as sub_status + ``` + + */ +@gentype +module DuplicateAliasTest: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, duplicateAliasTestParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, duplicateAliasTestParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + duplicateAliasTestParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, duplicateAliasTestParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external duplicateAliasTest: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = duplicateAliasTest(duplicateAliasTestIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'DuplicateAliasTest.many' directly instead") +let duplicateAliasTest = (params, ~client) => DuplicateAliasTest.many(client, params) + + +/** 'UnionTest' parameters type */ +@gentype +type unionTestParams = unit + +/** 'UnionTest' return type */ +@gentype +type unionTestResult = { + document_status: option, + version: option, +} + +/** 'UnionTest' query type */ +@gentype +type unionTestQuery = { + params: unionTestParams, + result: unionTestResult, +} + +%%private(let unionTestIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select 'draft' as document_status, 1 as version\n union all\n select 'published' as document_status, 2 as version"}`)) + +/** + Runnable query: + ```sql +select 'draft' as document_status, 1 as version + union all + select 'published' as document_status, 2 as version + ``` + + */ +@gentype +module UnionTest: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, unionTestParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, unionTestParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + unionTestParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, unionTestParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external unionTest: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = unionTest(unionTestIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'UnionTest.many' directly instead") +let unionTest = (params, ~client) => UnionTest.many(client, params) + + +/** 'SingleLiterals' parameters type */ +@gentype +type singleLiteralsParams = unit + +/** 'SingleLiterals' return type */ +@gentype +type singleLiteralsResult = { + booking_status: [#"confirmed"], + rating_stars: [#5], + subscription_tier: [#"premium"], +} + +/** 'SingleLiterals' query type */ +@gentype +type singleLiteralsQuery = { + params: singleLiteralsParams, + result: singleLiteralsResult, +} + +%%private(let singleLiteralsIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n 'confirmed' as booking_status,\n 5 as rating_stars,\n 'premium' as subscription_tier"}`)) + +/** + Runnable query: + ```sql +select + 'confirmed' as booking_status, + 5 as rating_stars, + 'premium' as subscription_tier + ``` + + */ +@gentype +module SingleLiterals: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, singleLiteralsParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, singleLiteralsParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + singleLiteralsParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, singleLiteralsParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external singleLiterals: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = singleLiterals(singleLiteralsIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'SingleLiterals.many' directly instead") +let singleLiterals = (params, ~client) => SingleLiterals.many(client, params) + + +/** 'EdgeCases' parameters type */ +@gentype +type edgeCasesParams = unit + +/** 'EdgeCases' return type */ +@gentype +type edgeCasesResult = { + empty_string: [#""], + large_number: [#123456789], + minus_one: option, + negative_number: option, + null_string: [#"null"], + single_char: [#"a"], + string_with_spaces: [#"with spaces"], + zero: [#0], +} + +/** 'EdgeCases' query type */ +@gentype +type edgeCasesQuery = { + params: edgeCasesParams, + result: edgeCasesResult, +} + +%%private(let edgeCasesIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n '' as empty_string,\n -999 as negative_number,\n 0 as zero,\n 'null' as null_string,\n 123456789 as large_number,\n 'a' as single_char,\n 'with spaces' as string_with_spaces,\n -1 as minus_one"}`)) + +/** + Runnable query: + ```sql +select + '' as empty_string, + -999 as negative_number, + 0 as zero, + 'null' as null_string, + 123456789 as large_number, + 'a' as single_char, + 'with spaces' as string_with_spaces, + -1 as minus_one + ``` + + */ +@gentype +module EdgeCases: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, edgeCasesParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, edgeCasesParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + edgeCasesParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, edgeCasesParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external edgeCases: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = edgeCases(edgeCasesIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'EdgeCases.many' directly instead") +let edgeCases = (params, ~client) => EdgeCases.many(client, params) + + diff --git a/packages/query/src/actions.ts b/packages/query/src/actions.ts index 36c0ac31..7fcbb270 100644 --- a/packages/query/src/actions.ts +++ b/packages/query/src/actions.ts @@ -12,7 +12,7 @@ import { createInitialSASLResponse, } from './sasl-helpers.js'; import { DatabaseTypeKind, isEnum, MappableType } from './type.js'; -import { parse, astVisitor, Expr } from 'pgsql-ast-parser'; +import { parse, astVisitor, Expr, Statement } from 'pgsql-ast-parser'; const debugQuery = debugBase('client:query'); @@ -411,9 +411,16 @@ interface ColumnCheck { } export type ConstraintValue = - | { type: 'string'; value: string } - | { type: 'integer'; value: number } - | { type: 'float'; value: number }; + | { + type: 'string'; + value: string; + alias?: string; + } + | { + type: 'integer'; + value: number; + alias?: string; + }; export function parseCheckAllowedValues(def: string): ConstraintValue[] | null { try { @@ -435,7 +442,11 @@ export function parseCheckAllowedValues(def: string): ConstraintValue[] | null { } else if (node.type === 'numeric' && 'value' in node) { values.push({ type: 'float', value: node.value });*/ - } else if (node.type === 'integer' && 'value' in node) { + } else if ( + node.type === 'integer' && + 'value' in node && + node.value >= 0 + ) { values.push({ type: 'integer', value: node.value }); } else { hadInvalidValue = true; @@ -526,6 +537,52 @@ async function getComments( })); } +export function getAliasedLiterals( + ast: Statement[], +): Map { + const values: ConstraintValue[] = []; + + const visitor = astVisitor((map) => ({ + selectionColumn: (node) => { + const { alias, expr } = node; + if (alias != null) { + if (expr.type === 'string' && 'value' in expr) { + values.push({ + type: 'string', + value: expr.value, + alias: alias.name, + }); + } else if ( + expr.type === 'integer' && + 'value' in expr && + expr.value >= 0 + ) { + values.push({ + type: 'integer', + value: expr.value, + alias: alias.name, + }); + } + } + map.super().selectionColumn(node); + }, + })); + + visitor.statement(ast[0]); + + const map = new Map(); + for (const v of values) { + // Duplicates means something is fishy, so we opt out for now + if (map.has(v.alias!)) { + map.delete(v.alias!); + } else { + map.set(v.alias!, v); + } + } + + return map; +} + export async function getTypes( queryData: InterpolatedQuery, queue: AsyncQueue, @@ -544,6 +601,8 @@ export async function getTypes( const commentRows = await getComments(fields, queue); const checkRows = await getCheckConstraints(fields, queue); const typeMap = reduceTypeRows(typeRows); + const parsedQuery = parse(queryData.query); + const aliasedLiterals = getAliasedLiterals(parsedQuery); const attrMatcher = ({ tableOID, @@ -590,13 +649,25 @@ export async function getTypes( checkMap[`${chk.tableOID}:${chk.columnAttrNumber}`] = chk.values; } - const returnTypes = fields.map((f) => ({ - ...attrMap[getAttid(f)], - ...(commentMap[getAttid(f)] ? { comment: commentMap[getAttid(f)] } : {}), - ...(checkMap[getAttid(f)] ? { checkValues: checkMap[getAttid(f)] } : {}), - returnName: f.name, - type: typeMap[f.typeOID], - })); + const returnTypes = fields + .map((f) => ({ + ...attrMap[getAttid(f)], + ...(commentMap[getAttid(f)] ? { comment: commentMap[getAttid(f)] } : {}), + ...(checkMap[getAttid(f)] ? { checkValues: checkMap[getAttid(f)] } : {}), + returnName: f.name, + type: typeMap[f.typeOID], + })) + .map((f) => { + const aliased = aliasedLiterals.get(f.returnName); + if (aliased != null) { + // Aliased literals are not nullable by definition + f.nullable = false; + f.checkValues = [aliased]; + return f; + } else { + return f; + } + }); const paramMetadata = { params: params.map(({ oid }) => typeMap[oid]), diff --git a/packages/runtime/src/preprocessor-sql.ts b/packages/runtime/src/preprocessor-sql.ts index 5259bbdd..6bb051d9 100644 --- a/packages/runtime/src/preprocessor-sql.ts +++ b/packages/runtime/src/preprocessor-sql.ts @@ -14,6 +14,7 @@ import { /* Processes query AST formed by new parser from pure SQL files */ export const processSQLQueryIR = ( queryIR: SQLQueryIR, + queryName: string | undefined, passedParams?: QueryParameters, ): InterpolatedQuery => { const bindings: Scalar[] = []; @@ -169,5 +170,6 @@ export const processSQLQueryIR = ( mapping: paramMapping, query: flatStr, bindings, + name: queryName, }; }; diff --git a/packages/runtime/src/preprocessor-ts.ts b/packages/runtime/src/preprocessor-ts.ts index 85a3326b..3485f1df 100644 --- a/packages/runtime/src/preprocessor-ts.ts +++ b/packages/runtime/src/preprocessor-ts.ts @@ -271,5 +271,6 @@ export const processTSQueryAST = ( mapping: parameters ? [] : Object.values(baseMap), query: flatStr, bindings, + name: query.name, }; }; diff --git a/packages/runtime/src/preprocessor.ts b/packages/runtime/src/preprocessor.ts index 8168ba32..b1adb354 100644 --- a/packages/runtime/src/preprocessor.ts +++ b/packages/runtime/src/preprocessor.ts @@ -46,6 +46,7 @@ export interface InterpolatedQuery { query: string; mapping: QueryParameter[]; bindings: Scalar[]; + name: string | undefined; } export interface NestedParameters { From bd3397b9d88ae68c2489d862ca40035f3b1144bd Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 21 Jun 2025 11:49:43 +0200 Subject: [PATCH 2/7] improve inference and handle union/union all --- packages/example/src/books/Misc.res | 28 +++- packages/example/src/books/Misc__sql.res | 178 ++++++++++++++++++++++- packages/query/src/actions.ts | 155 +++++++++++++++----- 3 files changed, 321 insertions(+), 40 deletions(-) diff --git a/packages/example/src/books/Misc.res b/packages/example/src/books/Misc.res index b84c2f3c..8cb9c47e 100644 --- a/packages/example/src/books/Misc.res +++ b/packages/example/src/books/Misc.res @@ -31,8 +31,6 @@ let duplicateAliasTest = %sql.one(` (select 'sub' as status) as sub_status `) -// Test with UNION - another case where same alias might appear -// Same aliases should prevent inference let unionTest = %sql.many(` /* @name UnionTest */ select 'draft' as document_status, 1 as version @@ -40,6 +38,15 @@ let unionTest = %sql.many(` select 'published' as document_status, 2 as version `) +let unionTestWithString = %sql.many(` + /* @name UnionTestWithString */ + select 'draft' as document_status, 1 as version + union all + select 'published' as document_status, 2 as version + union all + select 'draft' || 'two' as document_status, 3 as version +`) + // Test with single literals that should work let singleLiterals = %sql.one(` /* @name SingleLiterals */ @@ -62,3 +69,20 @@ let edgeCases = %sql.one(` 'with spaces' as string_with_spaces, -1 as minus_one `) + +// Test context tracking - literals in nested contexts shouldn't interfere +let contextTest = %sql.one(` + /* @name ContextTest */ + select + 'outer_result' as status, + 'final_value' as result_type, + ( + select count(*) + from ( + select 'inner_result' as status, -- Same alias but different context + 'internal_value' as result_type -- Same alias but different context + from generate_series(1,3) + ) inner_table + where inner_table.status = 'inner_result' -- This literal is used for filtering + ) as nested_count +`) diff --git a/packages/example/src/books/Misc__sql.res b/packages/example/src/books/Misc__sql.res index 3141067e..ac807789 100644 --- a/packages/example/src/books/Misc__sql.res +++ b/packages/example/src/books/Misc__sql.res @@ -188,7 +188,7 @@ type duplicateAliasTestParams = unit @gentype type duplicateAliasTestResult = { priority: [#1], - status: option, + status: [#"main"], sub_status: option, } @@ -269,8 +269,8 @@ type unionTestParams = unit /** 'UnionTest' return type */ @gentype type unionTestResult = { - document_status: option, - version: option, + document_status: [#"draft" | #"published"], + version: [#1 | #2], } /** 'UnionTest' query type */ @@ -342,6 +342,88 @@ module UnionTest: { let unionTest = (params, ~client) => UnionTest.many(client, params) +/** 'UnionTestWithString' parameters type */ +@gentype +type unionTestWithStringParams = unit + +/** 'UnionTestWithString' return type */ +@gentype +type unionTestWithStringResult = { + document_status: option, + version: [#1 | #2 | #3], +} + +/** 'UnionTestWithString' query type */ +@gentype +type unionTestWithStringQuery = { + params: unionTestWithStringParams, + result: unionTestWithStringResult, +} + +%%private(let unionTestWithStringIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select 'draft' as document_status, 1 as version\n union all\n select 'published' as document_status, 2 as version\n union all\n select 'draft' || 'two' as document_status, 3 as version"}`)) + +/** + Runnable query: + ```sql +select 'draft' as document_status, 1 as version + union all + select 'published' as document_status, 2 as version + union all + select 'draft' || 'two' as document_status, 3 as version + ``` + + */ +@gentype +module UnionTestWithString: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, unionTestWithStringParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, unionTestWithStringParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + unionTestWithStringParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, unionTestWithStringParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external unionTestWithString: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = unionTestWithString(unionTestWithStringIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'UnionTestWithString.many' directly instead") +let unionTestWithString = (params, ~client) => UnionTestWithString.many(client, params) + + /** 'SingleLiterals' parameters type */ @gentype type singleLiteralsParams = unit @@ -516,3 +598,93 @@ module EdgeCases: { let edgeCases = (params, ~client) => EdgeCases.many(client, params) +/** 'ContextTest' parameters type */ +@gentype +type contextTestParams = unit + +/** 'ContextTest' return type */ +@gentype +type contextTestResult = { + nested_count: option, + result_type: [#"final_value"], + status: [#"outer_result"], +} + +/** 'ContextTest' query type */ +@gentype +type contextTestQuery = { + params: contextTestParams, + result: contextTestResult, +} + +%%private(let contextTestIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select \n 'outer_result' as status,\n 'final_value' as result_type,\n (\n select count(*)\n from (\n select 'inner_result' as status, -- Same alias but different context\n 'internal_value' as result_type -- Same alias but different context\n from generate_series(1,3)\n ) inner_table\n where inner_table.status = 'inner_result' -- This literal is used for filtering\n ) as nested_count"}`)) + +/** + Runnable query: + ```sql +select + 'outer_result' as status, + 'final_value' as result_type, + ( + select count(*) + from ( + select 'inner_result' as status, -- Same alias but different context + 'internal_value' as result_type -- Same alias but different context + from generate_series(1,3) + ) inner_table + where inner_table.status = 'inner_result' -- This literal is used for filtering + ) as nested_count + ``` + + */ +@gentype +module ContextTest: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, contextTestParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, contextTestParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + contextTestParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, contextTestParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external contextTest: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = contextTest(contextTestIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'ContextTest.many' directly instead") +let contextTest = (params, ~client) => ContextTest.many(client, params) + + diff --git a/packages/query/src/actions.ts b/packages/query/src/actions.ts index 7fcbb270..cf562b33 100644 --- a/packages/query/src/actions.ts +++ b/packages/query/src/actions.ts @@ -12,7 +12,13 @@ import { createInitialSASLResponse, } from './sasl-helpers.js'; import { DatabaseTypeKind, isEnum, MappableType } from './type.js'; -import { parse, astVisitor, Expr, Statement } from 'pgsql-ast-parser'; +import { + parse, + astVisitor, + Expr, + Statement, + SelectStatement, +} from 'pgsql-ast-parser'; const debugQuery = debugBase('client:query'); @@ -415,11 +421,13 @@ export type ConstraintValue = type: 'string'; value: string; alias?: string; + context?: string; } | { type: 'integer'; value: number; alias?: string; + context?: string; }; export function parseCheckAllowedValues(def: string): ConstraintValue[] | null { @@ -539,47 +547,124 @@ async function getComments( export function getAliasedLiterals( ast: Statement[], -): Map { +): Map { const values: ConstraintValue[] = []; + const aliasesWithInvalidValues = new Set(); + const topStatement = ast[0]; + + if (topStatement.type === 'union' || topStatement.type === 'union all') { + const unionContext = 'union'; + + const collectFromSelect = (selectNode: SelectStatement) => { + const visitor = astVisitor((map) => ({ + selectionColumn: (node) => { + const { alias, expr } = node; + if (alias != null) { + if (expr.type === 'string' && 'value' in expr) { + values.push({ + type: 'string', + value: expr.value, + alias: alias.name, + context: unionContext, + }); + } else if ( + expr.type === 'integer' && + 'value' in expr && + expr.value >= 0 + ) { + values.push({ + type: 'integer', + value: expr.value, + alias: alias.name, + context: unionContext, + }); + } else { + aliasesWithInvalidValues.add(alias.name); + } + } + map.super().selectionColumn(node); + }, + })); + visitor.select(selectNode); + }; - const visitor = astVisitor((map) => ({ - selectionColumn: (node) => { - const { alias, expr } = node; - if (alias != null) { - if (expr.type === 'string' && 'value' in expr) { - values.push({ - type: 'string', - value: expr.value, - alias: alias.name, - }); - } else if ( - expr.type === 'integer' && - 'value' in expr && - expr.value >= 0 - ) { - values.push({ - type: 'integer', - value: expr.value, - alias: alias.name, - }); - } + // Helper to traverse union nodes + const traverseUnion = (node: Expr) => { + if (node.type === 'select') { + collectFromSelect(node); + } else if (node.type === 'union' || node.type === 'union all') { + traverseUnion(node.left); + traverseUnion(node.right); } - map.super().selectionColumn(node); - }, - })); + }; - visitor.statement(ast[0]); + traverseUnion(topStatement); + } else { + if (topStatement.type === 'select') { + // Do not traverse any nested structure. + const columns = topStatement.columns || []; + for (const column of columns) { + if (column.alias && column.expr) { + const { alias, expr } = column; + if (expr.type === 'string' && 'value' in expr) { + values.push({ + type: 'string', + value: expr.value, + alias: alias.name, + context: 'select', + }); + } else if ( + expr.type === 'integer' && + 'value' in expr && + expr.value >= 0 + ) { + values.push({ + type: 'integer', + value: expr.value, + alias: alias.name, + context: 'select', + }); + } else { + aliasesWithInvalidValues.add(alias.name); + } + } + } + } + } - const map = new Map(); + const map = new Map(); for (const v of values) { - // Duplicates means something is fishy, so we opt out for now - if (map.has(v.alias!)) { - map.delete(v.alias!); + const key = v.alias!; + if (map.has(key)) { + const existing = map.get(key)!; + + // Only accumulate if from the same context (same logical level) + const sameContext = + existing.length > 0 && existing[0].context === v.context; + if (sameContext) { + // Only add if the value isn't already present (avoid true duplicates) + const isDuplicate = existing.some( + (existing) => existing.type === v.type && existing.value === v.value, + ); + if (!isDuplicate) { + existing.push(v); + } + } else { + // Different contexts means different logical meanings, skip inference + map.delete(key); + } } else { - map.set(v.alias!, v); + map.set(key, [v]); } } + // Opt out of literal inference for aliases with some non-literal values. + // In the future this could be extended to use unboxed variants, and we could capture the + // "other" value efficiently as a catch-all. + for (const alias of aliasesWithInvalidValues) { + map.delete(alias); + } + return map; } @@ -658,11 +743,11 @@ export async function getTypes( type: typeMap[f.typeOID], })) .map((f) => { - const aliased = aliasedLiterals.get(f.returnName); - if (aliased != null) { + const aliasedValues = aliasedLiterals.get(f.returnName); + if (aliasedValues != null) { // Aliased literals are not nullable by definition f.nullable = false; - f.checkValues = [aliased]; + f.checkValues = aliasedValues; return f; } else { return f; From fab0252b9e49890a1d105e41eb51792135843879 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 21 Jun 2025 11:51:17 +0200 Subject: [PATCH 3/7] generate --- .../src/books/BookService__sql.gen.tsx | 44 +-- packages/example/src/books/Misc__sql.gen.tsx | 300 +++++++++++++++++- 2 files changed, 321 insertions(+), 23 deletions(-) diff --git a/packages/example/src/books/BookService__sql.gen.tsx b/packages/example/src/books/BookService__sql.gen.tsx index fee7ce98..ee4d5fb1 100644 --- a/packages/example/src/books/BookService__sql.gen.tsx +++ b/packages/example/src/books/BookService__sql.gen.tsx @@ -11,11 +11,11 @@ export type category = "novel" | "science-fiction" | "thriller"; export type categoryArray = category[]; -/** 'BooksByAuthor' parameters type */ -export type booksByAuthorParams = { readonly authorName: string }; +/** 'FindBookById' parameters type */ +export type findBookByIdParams = { readonly id?: number }; -/** 'BooksByAuthor' return type */ -export type booksByAuthorResult = { +/** 'FindBookById' return type */ +export type findBookByIdResult = { readonly author_id: (undefined | number); readonly categories: (undefined | categoryArray); readonly id: number; @@ -23,14 +23,14 @@ export type booksByAuthorResult = { readonly rank: (undefined | number) }; -/** 'BooksByAuthor' query type */ -export type booksByAuthorQuery = { readonly params: booksByAuthorParams; readonly result: booksByAuthorResult }; +/** 'FindBookById' query type */ +export type findBookByIdQuery = { readonly params: findBookByIdParams; readonly result: findBookByIdResult }; -/** 'FindBookById' parameters type */ -export type findBookByIdParams = { readonly id?: number }; +/** 'BooksByAuthor' parameters type */ +export type booksByAuthorParams = { readonly authorName: string }; -/** 'FindBookById' return type */ -export type findBookByIdResult = { +/** 'BooksByAuthor' return type */ +export type booksByAuthorResult = { readonly author_id: (undefined | number); readonly categories: (undefined | categoryArray); readonly id: number; @@ -38,36 +38,36 @@ export type findBookByIdResult = { readonly rank: (undefined | number) }; -/** 'FindBookById' query type */ -export type findBookByIdQuery = { readonly params: findBookByIdParams; readonly result: findBookByIdResult }; +/** 'BooksByAuthor' query type */ +export type booksByAuthorQuery = { readonly params: booksByAuthorParams; readonly result: booksByAuthorResult }; /** Returns an array of all matched results. */ -export const BooksByAuthor_many: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise = BookService__sqlJS.BooksByAuthor.many as any; +export const FindBookById_many: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams) => Promise = BookService__sqlJS.FindBookById.many as any; /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ -export const BooksByAuthor_one: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise<(undefined | booksByAuthorResult)> = BookService__sqlJS.BooksByAuthor.one as any; +export const FindBookById_one: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams) => Promise<(undefined | findBookByIdResult)> = BookService__sqlJS.FindBookById.one as any; /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ -export const BooksByAuthor_expectOne: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams, errorMessage:(undefined | string)) => Promise = BookService__sqlJS.BooksByAuthor.expectOne as any; +export const FindBookById_expectOne: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams, errorMessage:(undefined | string)) => Promise = BookService__sqlJS.FindBookById.expectOne as any; /** Executes the query, but ignores whatever is returned by it. */ -export const BooksByAuthor_execute: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise = BookService__sqlJS.BooksByAuthor.execute as any; +export const FindBookById_execute: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams) => Promise = BookService__sqlJS.FindBookById.execute as any; -export const booksByAuthor: (params:booksByAuthorParams, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.booksByAuthor as any; +export const findBookById: (params:findBookByIdParams, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.findBookById as any; /** Returns an array of all matched results. */ -export const FindBookById_many: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams) => Promise = BookService__sqlJS.FindBookById.many as any; +export const BooksByAuthor_many: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise = BookService__sqlJS.BooksByAuthor.many as any; /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ -export const FindBookById_one: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams) => Promise<(undefined | findBookByIdResult)> = BookService__sqlJS.FindBookById.one as any; +export const BooksByAuthor_one: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise<(undefined | booksByAuthorResult)> = BookService__sqlJS.BooksByAuthor.one as any; /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ -export const FindBookById_expectOne: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams, errorMessage:(undefined | string)) => Promise = BookService__sqlJS.FindBookById.expectOne as any; +export const BooksByAuthor_expectOne: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams, errorMessage:(undefined | string)) => Promise = BookService__sqlJS.BooksByAuthor.expectOne as any; /** Executes the query, but ignores whatever is returned by it. */ -export const FindBookById_execute: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams) => Promise = BookService__sqlJS.FindBookById.execute as any; +export const BooksByAuthor_execute: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise = BookService__sqlJS.BooksByAuthor.execute as any; -export const findBookById: (params:findBookByIdParams, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.findBookById as any; +export const booksByAuthor: (params:booksByAuthorParams, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.booksByAuthor as any; export const FindBookById: { /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ diff --git a/packages/example/src/books/Misc__sql.gen.tsx b/packages/example/src/books/Misc__sql.gen.tsx index 3b38793e..d5ef3c8d 100644 --- a/packages/example/src/books/Misc__sql.gen.tsx +++ b/packages/example/src/books/Misc__sql.gen.tsx @@ -11,11 +11,134 @@ import type {Pg_Client_t as PgTyped_Pg_Client_t} from 'pgtyped-rescript/src/res/ export type literalsParams = void; /** 'Literals' return type */ -export type literalsResult = { readonly integer_literal: (undefined | number); readonly literal: (undefined | string) }; +export type literalsResult = { + readonly test_integer_literal: + 1; + readonly test_regular_string: (undefined | string); + readonly test_string_literal: + "literal" +}; /** 'Literals' query type */ export type literalsQuery = { readonly params: literalsParams; readonly result: literalsResult }; +/** 'MoreLiterals' parameters type */ +export type moreLiteralsParams = void; + +/** 'MoreLiterals' return type */ +export type moreLiteralsResult = { + readonly admin_role: + "admin"; + readonly error_status: + "error"; + readonly guest_role: + "guest"; + readonly magic_number: + 42; + readonly max_percentage: + 100; + readonly negative_one: (undefined | number); + readonly pending_status: + "pending"; + readonly status: + "success"; + readonly user_role: + "user"; + readonly zero_value: + 0 +}; + +/** 'MoreLiterals' query type */ +export type moreLiteralsQuery = { readonly params: moreLiteralsParams; readonly result: moreLiteralsResult }; + +/** 'DuplicateAliasTest' parameters type */ +export type duplicateAliasTestParams = void; + +/** 'DuplicateAliasTest' return type */ +export type duplicateAliasTestResult = { + readonly priority: + 1; + readonly status: + "main"; + readonly sub_status: (undefined | string) +}; + +/** 'DuplicateAliasTest' query type */ +export type duplicateAliasTestQuery = { readonly params: duplicateAliasTestParams; readonly result: duplicateAliasTestResult }; + +/** 'UnionTest' parameters type */ +export type unionTestParams = void; + +/** 'UnionTest' return type */ +export type unionTestResult = { readonly document_status: "draft" | "published"; readonly version: 2 | 1 }; + +/** 'UnionTest' query type */ +export type unionTestQuery = { readonly params: unionTestParams; readonly result: unionTestResult }; + +/** 'UnionTestWithString' parameters type */ +export type unionTestWithStringParams = void; + +/** 'UnionTestWithString' return type */ +export type unionTestWithStringResult = { readonly document_status: (undefined | string); readonly version: 2 | 1 | 3 }; + +/** 'UnionTestWithString' query type */ +export type unionTestWithStringQuery = { readonly params: unionTestWithStringParams; readonly result: unionTestWithStringResult }; + +/** 'SingleLiterals' parameters type */ +export type singleLiteralsParams = void; + +/** 'SingleLiterals' return type */ +export type singleLiteralsResult = { + readonly booking_status: + "confirmed"; + readonly rating_stars: + 5; + readonly subscription_tier: + "premium" +}; + +/** 'SingleLiterals' query type */ +export type singleLiteralsQuery = { readonly params: singleLiteralsParams; readonly result: singleLiteralsResult }; + +/** 'EdgeCases' parameters type */ +export type edgeCasesParams = void; + +/** 'EdgeCases' return type */ +export type edgeCasesResult = { + readonly empty_string: + ""; + readonly large_number: + 123456789; + readonly minus_one: (undefined | number); + readonly negative_number: (undefined | number); + readonly null_string: + "null"; + readonly single_char: + "a"; + readonly string_with_spaces: + "with spaces"; + readonly zero: + 0 +}; + +/** 'EdgeCases' query type */ +export type edgeCasesQuery = { readonly params: edgeCasesParams; readonly result: edgeCasesResult }; + +/** 'ContextTest' parameters type */ +export type contextTestParams = void; + +/** 'ContextTest' return type */ +export type contextTestResult = { + readonly nested_count: (undefined | bigint); + readonly result_type: + "final_value"; + readonly status: + "outer_result" +}; + +/** 'ContextTest' query type */ +export type contextTestQuery = { readonly params: contextTestParams; readonly result: contextTestResult }; + /** Returns an array of all matched results. */ export const Literals_many: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Promise = Misc__sqlJS.Literals.many as any; @@ -30,6 +153,137 @@ export const Literals_execute: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Pr export const literals: (params:literalsParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.literals as any; +/** Returns an array of all matched results. */ +export const MoreLiterals_many: (_1:PgTyped_Pg_Client_t, _2:moreLiteralsParams) => Promise = Misc__sqlJS.MoreLiterals.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const MoreLiterals_one: (_1:PgTyped_Pg_Client_t, _2:moreLiteralsParams) => Promise<(undefined | moreLiteralsResult)> = Misc__sqlJS.MoreLiterals.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const MoreLiterals_expectOne: (_1:PgTyped_Pg_Client_t, _2:moreLiteralsParams, errorMessage:(undefined | string)) => Promise = Misc__sqlJS.MoreLiterals.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const MoreLiterals_execute: (_1:PgTyped_Pg_Client_t, _2:moreLiteralsParams) => Promise = Misc__sqlJS.MoreLiterals.execute as any; + +export const moreLiterals: (params:moreLiteralsParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.moreLiterals as any; + +/** Returns an array of all matched results. */ +export const DuplicateAliasTest_many: (_1:PgTyped_Pg_Client_t, _2:duplicateAliasTestParams) => Promise = Misc__sqlJS.DuplicateAliasTest.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const DuplicateAliasTest_one: (_1:PgTyped_Pg_Client_t, _2:duplicateAliasTestParams) => Promise<(undefined | duplicateAliasTestResult)> = Misc__sqlJS.DuplicateAliasTest.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const DuplicateAliasTest_expectOne: (_1:PgTyped_Pg_Client_t, _2:duplicateAliasTestParams, errorMessage:(undefined | string)) => Promise = Misc__sqlJS.DuplicateAliasTest.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const DuplicateAliasTest_execute: (_1:PgTyped_Pg_Client_t, _2:duplicateAliasTestParams) => Promise = Misc__sqlJS.DuplicateAliasTest.execute as any; + +export const duplicateAliasTest: (params:duplicateAliasTestParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.duplicateAliasTest as any; + +/** Returns an array of all matched results. */ +export const UnionTest_many: (_1:PgTyped_Pg_Client_t, _2:unionTestParams) => Promise = Misc__sqlJS.UnionTest.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const UnionTest_one: (_1:PgTyped_Pg_Client_t, _2:unionTestParams) => Promise<(undefined | unionTestResult)> = Misc__sqlJS.UnionTest.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const UnionTest_expectOne: (_1:PgTyped_Pg_Client_t, _2:unionTestParams, errorMessage:(undefined | string)) => Promise = Misc__sqlJS.UnionTest.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const UnionTest_execute: (_1:PgTyped_Pg_Client_t, _2:unionTestParams) => Promise = Misc__sqlJS.UnionTest.execute as any; + +export const unionTest: (params:unionTestParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.unionTest as any; + +/** Returns an array of all matched results. */ +export const UnionTestWithString_many: (_1:PgTyped_Pg_Client_t, _2:unionTestWithStringParams) => Promise = Misc__sqlJS.UnionTestWithString.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const UnionTestWithString_one: (_1:PgTyped_Pg_Client_t, _2:unionTestWithStringParams) => Promise<(undefined | unionTestWithStringResult)> = Misc__sqlJS.UnionTestWithString.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const UnionTestWithString_expectOne: (_1:PgTyped_Pg_Client_t, _2:unionTestWithStringParams, errorMessage:(undefined | string)) => Promise = Misc__sqlJS.UnionTestWithString.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const UnionTestWithString_execute: (_1:PgTyped_Pg_Client_t, _2:unionTestWithStringParams) => Promise = Misc__sqlJS.UnionTestWithString.execute as any; + +export const unionTestWithString: (params:unionTestWithStringParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.unionTestWithString as any; + +/** Returns an array of all matched results. */ +export const SingleLiterals_many: (_1:PgTyped_Pg_Client_t, _2:singleLiteralsParams) => Promise = Misc__sqlJS.SingleLiterals.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const SingleLiterals_one: (_1:PgTyped_Pg_Client_t, _2:singleLiteralsParams) => Promise<(undefined | singleLiteralsResult)> = Misc__sqlJS.SingleLiterals.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const SingleLiterals_expectOne: (_1:PgTyped_Pg_Client_t, _2:singleLiteralsParams, errorMessage:(undefined | string)) => Promise = Misc__sqlJS.SingleLiterals.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const SingleLiterals_execute: (_1:PgTyped_Pg_Client_t, _2:singleLiteralsParams) => Promise = Misc__sqlJS.SingleLiterals.execute as any; + +export const singleLiterals: (params:singleLiteralsParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.singleLiterals as any; + +/** Returns an array of all matched results. */ +export const EdgeCases_many: (_1:PgTyped_Pg_Client_t, _2:edgeCasesParams) => Promise = Misc__sqlJS.EdgeCases.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const EdgeCases_one: (_1:PgTyped_Pg_Client_t, _2:edgeCasesParams) => Promise<(undefined | edgeCasesResult)> = Misc__sqlJS.EdgeCases.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const EdgeCases_expectOne: (_1:PgTyped_Pg_Client_t, _2:edgeCasesParams, errorMessage:(undefined | string)) => Promise = Misc__sqlJS.EdgeCases.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const EdgeCases_execute: (_1:PgTyped_Pg_Client_t, _2:edgeCasesParams) => Promise = Misc__sqlJS.EdgeCases.execute as any; + +export const edgeCases: (params:edgeCasesParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.edgeCases as any; + +/** Returns an array of all matched results. */ +export const ContextTest_many: (_1:PgTyped_Pg_Client_t, _2:contextTestParams) => Promise = Misc__sqlJS.ContextTest.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const ContextTest_one: (_1:PgTyped_Pg_Client_t, _2:contextTestParams) => Promise<(undefined | contextTestResult)> = Misc__sqlJS.ContextTest.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const ContextTest_expectOne: (_1:PgTyped_Pg_Client_t, _2:contextTestParams, errorMessage:(undefined | string)) => Promise = Misc__sqlJS.ContextTest.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const ContextTest_execute: (_1:PgTyped_Pg_Client_t, _2:contextTestParams) => Promise = Misc__sqlJS.ContextTest.execute as any; + +export const contextTest: (params:contextTestParams, client:PgTyped_Pg_Client_t) => Promise = Misc__sqlJS.contextTest as any; + +export const MoreLiterals: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:moreLiteralsParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:moreLiteralsParams) => Promise<(undefined | moreLiteralsResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:moreLiteralsParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:moreLiteralsParams) => Promise +} = Misc__sqlJS.MoreLiterals as any; + +export const UnionTest: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:unionTestParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:unionTestParams) => Promise<(undefined | unionTestResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:unionTestParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:unionTestParams) => Promise +} = Misc__sqlJS.UnionTest as any; + +export const EdgeCases: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:edgeCasesParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:edgeCasesParams) => Promise<(undefined | edgeCasesResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:edgeCasesParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:edgeCasesParams) => Promise +} = Misc__sqlJS.EdgeCases as any; + export const Literals: { /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ expectOne: (_1:PgTyped_Pg_Client_t, _2:literalsParams, errorMessage:(undefined | string)) => Promise; @@ -40,3 +294,47 @@ export const Literals: { /** Executes the query, but ignores whatever is returned by it. */ execute: (_1:PgTyped_Pg_Client_t, _2:literalsParams) => Promise } = Misc__sqlJS.Literals as any; + +export const DuplicateAliasTest: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:duplicateAliasTestParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:duplicateAliasTestParams) => Promise<(undefined | duplicateAliasTestResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:duplicateAliasTestParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:duplicateAliasTestParams) => Promise +} = Misc__sqlJS.DuplicateAliasTest as any; + +export const SingleLiterals: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:singleLiteralsParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:singleLiteralsParams) => Promise<(undefined | singleLiteralsResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:singleLiteralsParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:singleLiteralsParams) => Promise +} = Misc__sqlJS.SingleLiterals as any; + +export const UnionTestWithString: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:unionTestWithStringParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:unionTestWithStringParams) => Promise<(undefined | unionTestWithStringResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:unionTestWithStringParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:unionTestWithStringParams) => Promise +} = Misc__sqlJS.UnionTestWithString as any; + +export const ContextTest: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:contextTestParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:contextTestParams) => Promise<(undefined | contextTestResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:contextTestParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:contextTestParams) => Promise +} = Misc__sqlJS.ContextTest as any; From 2a4fef583fac20cd6286ca7ff5cbe4aef682f7f7 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 21 Jun 2025 11:55:04 +0200 Subject: [PATCH 4/7] changelog and docs --- CHANGELOG.md | 1 + RESCRIPT.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18e12870..c7a78943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Auto-escape all ReScript keywords in generated record field names. - Add automatic parsing of PostgreSQL check constraints to generate ReScript polyvariant types for enumeration-style constraints. Supports both `column IN (value1, value2, ...)` and `column = ANY (ARRAY[value1, value2, ...])` patterns with string and integer values. +- Add top-level literal inference for SELECT queries. When a query returns literal values with aliases (e.g., `SELECT 'success' as status, 42 as code`), PgTyped now automatically infers specific polyvariant types like `[#"success"]` and `[#42]` instead of generic `string` and `int` types. This provides better type safety and autocompletion. Also works with UNION queries where literals are consistent across all branches. - Remove dependency on `@rescript/core` since it's not really used. # 2.6.0 diff --git a/RESCRIPT.md b/RESCRIPT.md index ce3164a1..ca4da55a 100644 --- a/RESCRIPT.md +++ b/RESCRIPT.md @@ -170,6 +170,78 @@ let books = await client->GetBooksByStatus.many({ This feature works seamlessly with both separate SQL files and SQL-in-ReScript modes. +## Literal Type Inference + +`pgtyped-rescript` automatically infers specific polyvariant types for literal values in your SQL queries, providing enhanced type safety and better development experience. + +### How It Works + +When your SQL queries return literal values with aliases, `pgtyped-rescript` generates specific polyvariant types instead of generic `string`, `int`, etc. This works for both simple SELECT queries and UNION queries where literals are consistent across all branches. + +### Examples + +**Simple literals:** + +```sql +/* @name getStatus */ +SELECT + 'success' as status, + 200 as code, + 'active' as state +``` + +Generated ReScript type: + +```rescript +type getStatusResult = { + status: [#"success"], + code: [#200], + state: [#"active"], +} +``` + +**Union queries with consistent literals:** + +```sql +/* @name getDocumentStatus */ +SELECT 'draft' as status, 1 as version +UNION ALL +SELECT 'published' as status, 2 as version +``` + +Generated ReScript type: + +```rescript +type getDocumentStatusResult = { + status: [#"draft" | #"published"], + version: [#1 | #2], +} +``` + +**SQL-in-ReScript example:** + +```rescript +let getOrderStatus = %sql.many(` + SELECT + 'pending' as status, + 0 as priority + UNION ALL + SELECT + 'shipped' as status, + 1 as priority +`) + +// Returns: array<{status: [#"pending" | #"shipped"], priority: [#0 | #1]}> +let statuses = await client->getOrderStatus() +``` + +### Smart Inference Rules + +- **Consistent literals**: Only infers polyvariants when all literal values for the same alias are actual literals (not expressions) +- **Context-aware**: Handles nested queries correctly, only inferring from the top-level SELECT +- **Duplicate handling**: Automatically deduplicates identical literals in UNION queries +- **Mixed expressions**: Falls back to generic types when mixing literals with expressions (e.g., `'draft' || 'suffix'`) + ## API ### `PgTyped` From f35c81cf1c949be3fc34b71e71073187e23de92a Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 21 Jun 2025 11:59:38 +0200 Subject: [PATCH 5/7] lint --- packages/query/src/actions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/query/src/actions.ts b/packages/query/src/actions.ts index cf562b33..d9eb0c36 100644 --- a/packages/query/src/actions.ts +++ b/packages/query/src/actions.ts @@ -556,7 +556,7 @@ export function getAliasedLiterals( const unionContext = 'union'; const collectFromSelect = (selectNode: SelectStatement) => { - const visitor = astVisitor((map) => ({ + const visitor = astVisitor((v) => ({ selectionColumn: (node) => { const { alias, expr } = node; if (alias != null) { @@ -582,7 +582,7 @@ export function getAliasedLiterals( aliasesWithInvalidValues.add(alias.name); } } - map.super().selectionColumn(node); + v.super().selectionColumn(node); }, })); visitor.select(selectNode); @@ -644,7 +644,7 @@ export function getAliasedLiterals( if (sameContext) { // Only add if the value isn't already present (avoid true duplicates) const isDuplicate = existing.some( - (existing) => existing.type === v.type && existing.value === v.value, + (e) => e.type === v.type && e.value === v.value, ); if (!isDuplicate) { existing.push(v); From 91437b5691e4c648e8f11d02a1822fcf58274416 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 21 Jun 2025 13:01:11 +0200 Subject: [PATCH 6/7] refactor and fix tests --- packages/cli/src/generator.ts | 4 +- .../src/books/BookServiceParams2__sql.res | 2 +- .../src/books/BookServiceParams__sql.res | 2 +- .../example/src/books/BookService__sql.res | 4 +- packages/example/src/books/Dump__sql.res | 2 +- packages/example/src/books/Json__sql.res | 2 +- packages/example/src/books/Keywords__sql.res | 2 +- packages/example/src/books/Misc__sql.res | 16 ++-- packages/example/src/books/books__sql.res | 28 +++---- .../example/src/comments/comments__sql.res | 8 +- .../src/notifications/notifications__sql.res | 6 +- packages/parser/src/loader/sql/index.ts | 2 + packages/runtime/src/preprocessor-sql.test.ts | 78 +++++++++++++------ packages/runtime/src/preprocessor-sql.ts | 3 +- packages/runtime/src/preprocessor-ts.test.ts | 55 ++++++++----- 15 files changed, 132 insertions(+), 82 deletions(-) diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index 46d83d99..8ac26ef2 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -83,7 +83,7 @@ export async function queryToTypeDeclarations( queryData = processTSQueryAST(parsedQuery.ast); } else { queryName = pascalCase(parsedQuery.ast.name); - queryData = processSQLQueryIR(queryASTToIR(parsedQuery.ast), queryName); + queryData = processSQLQueryIR(queryASTToIR(parsedQuery.ast)); } const typeData = await typeSource(queryData); @@ -394,7 +394,7 @@ export async function generateDeclarationFile( `/**\n` + ` Runnable query:\n` + ` \`\`\`sql\n` + - `${processSQLQueryIR(typeDec.query.ir, typeDec.query.name).query}\n` + + `${processSQLQueryIR(typeDec.query.ir).query}\n` + ` \`\`\`\n\n` + ` */\n`; declarationFileContents += `@gentype diff --git a/packages/example/src/books/BookServiceParams2__sql.res b/packages/example/src/books/BookServiceParams2__sql.res index 0be41ac6..ef3278cc 100644 --- a/packages/example/src/books/BookServiceParams2__sql.res +++ b/packages/example/src/books/BookServiceParams2__sql.res @@ -28,7 +28,7 @@ type query1Query = { result: query1Result, } -%%private(let query1IR: IR.t = %raw(`{"usedParamSet":{"notification":true},"params":[{"name":"notification","required":false,"transform":{"type":"pick_tuple","keys":[{"name":"payload","required":false},{"name":"user_id","required":false},{"name":"type","required":false}]},"locs":[{"a":58,"b":70}]}],"statement":"INSERT INTO notifications (payload, user_id, type) VALUES :notification"}`)) +%%private(let query1IR: IR.t = %raw(`{"queryName":"Query1","usedParamSet":{"notification":true},"params":[{"name":"notification","required":false,"transform":{"type":"pick_tuple","keys":[{"name":"payload","required":false},{"name":"user_id","required":false},{"name":"type","required":false}]},"locs":[{"a":58,"b":70}]}],"statement":"INSERT INTO notifications (payload, user_id, type) VALUES :notification"}`)) /** Runnable query: diff --git a/packages/example/src/books/BookServiceParams__sql.res b/packages/example/src/books/BookServiceParams__sql.res index c7284256..543ee54b 100644 --- a/packages/example/src/books/BookServiceParams__sql.res +++ b/packages/example/src/books/BookServiceParams__sql.res @@ -28,7 +28,7 @@ type query1Query = { result: query1Result, } -%%private(let query1IR: IR.t = %raw(`{"usedParamSet":{"notification":true},"params":[{"name":"notification","required":false,"transform":{"type":"pick_tuple","keys":[{"name":"payload","required":false},{"name":"user_id","required":false},{"name":"type","required":false}]},"locs":[{"a":58,"b":70}]}],"statement":"INSERT INTO notifications (payload, user_id, type) VALUES :notification"}`)) +%%private(let query1IR: IR.t = %raw(`{"queryName":"Query1","usedParamSet":{"notification":true},"params":[{"name":"notification","required":false,"transform":{"type":"pick_tuple","keys":[{"name":"payload","required":false},{"name":"user_id","required":false},{"name":"type","required":false}]},"locs":[{"a":58,"b":70}]}],"statement":"INSERT INTO notifications (payload, user_id, type) VALUES :notification"}`)) /** Runnable query: diff --git a/packages/example/src/books/BookService__sql.res b/packages/example/src/books/BookService__sql.res index 92c2e916..51cfb8f1 100644 --- a/packages/example/src/books/BookService__sql.res +++ b/packages/example/src/books/BookService__sql.res @@ -31,7 +31,7 @@ type findBookByIdQuery = { result: findBookByIdResult, } -%%private(let findBookByIdIR: IR.t = %raw(`{"usedParamSet":{"id":true},"params":[{"name":"id","required":false,"transform":{"type":"scalar"},"locs":[{"a":31,"b":33}]}],"statement":"SELECT * FROM books WHERE id = :id"}`)) +%%private(let findBookByIdIR: IR.t = %raw(`{"queryName":"FindBookById","usedParamSet":{"id":true},"params":[{"name":"id","required":false,"transform":{"type":"scalar"},"locs":[{"a":31,"b":33}]}],"statement":"SELECT * FROM books WHERE id = :id"}`)) /** Runnable query: @@ -114,7 +114,7 @@ type booksByAuthorQuery = { result: booksByAuthorResult, } -%%private(let booksByAuthorIR: IR.t = %raw(`{"usedParamSet":{"authorName":true},"params":[{"name":"authorName","required":true,"transform":{"type":"scalar"},"locs":[{"a":118,"b":129}]}],"statement":"SELECT b.* FROM books b\n INNER JOIN authors a ON a.id = b.author_id\n WHERE a.first_name || ' ' || a.last_name = :authorName!"}`)) +%%private(let booksByAuthorIR: IR.t = %raw(`{"queryName":"BooksByAuthor","usedParamSet":{"authorName":true},"params":[{"name":"authorName","required":true,"transform":{"type":"scalar"},"locs":[{"a":118,"b":129}]}],"statement":"SELECT b.* FROM books b\n INNER JOIN authors a ON a.id = b.author_id\n WHERE a.first_name || ' ' || a.last_name = :authorName!"}`)) /** Runnable query: diff --git a/packages/example/src/books/Dump__sql.res b/packages/example/src/books/Dump__sql.res index 0bb997c5..1769191d 100644 --- a/packages/example/src/books/Dump__sql.res +++ b/packages/example/src/books/Dump__sql.res @@ -43,7 +43,7 @@ type dumpQuery = { result: dumpResult, } -%%private(let dumpIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT * FROM dump LIMIT 1"}`)) +%%private(let dumpIR: IR.t = %raw(`{"queryName":"Dump","usedParamSet":{},"params":[],"statement":"SELECT * FROM dump LIMIT 1"}`)) /** Runnable query: diff --git a/packages/example/src/books/Json__sql.res b/packages/example/src/books/Json__sql.res index 3fb77f1f..b8b50b4e 100644 --- a/packages/example/src/books/Json__sql.res +++ b/packages/example/src/books/Json__sql.res @@ -19,7 +19,7 @@ type jsonQuery = { result: jsonResult, } -%%private(let jsonIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT json_build_object('key', 'value') AS json_object"}`)) +%%private(let jsonIR: IR.t = %raw(`{"queryName":"Json","usedParamSet":{},"params":[],"statement":"SELECT json_build_object('key', 'value') AS json_object"}`)) /** Runnable query: diff --git a/packages/example/src/books/Keywords__sql.res b/packages/example/src/books/Keywords__sql.res index 0ff2c058..e5727a31 100644 --- a/packages/example/src/books/Keywords__sql.res +++ b/packages/example/src/books/Keywords__sql.res @@ -36,7 +36,7 @@ type keywordsQuery = { result: keywordsResult, } -%%private(let keywordsIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select * from rescript_keywords_need_to_be_escaped"}`)) +%%private(let keywordsIR: IR.t = %raw(`{"queryName":"Keywords","usedParamSet":{},"params":[],"statement":"select * from rescript_keywords_need_to_be_escaped"}`)) /** Runnable query: diff --git a/packages/example/src/books/Misc__sql.res b/packages/example/src/books/Misc__sql.res index ac807789..df1b8430 100644 --- a/packages/example/src/books/Misc__sql.res +++ b/packages/example/src/books/Misc__sql.res @@ -21,7 +21,7 @@ type literalsQuery = { result: literalsResult, } -%%private(let literalsIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n 'literal' as test_string_literal,\n 1 as test_integer_literal,\n 'hello ' || 'world' as test_regular_string"}`)) +%%private(let literalsIR: IR.t = %raw(`{"queryName":"Literals","usedParamSet":{},"params":[],"statement":"select\n 'literal' as test_string_literal,\n 1 as test_integer_literal,\n 'hello ' || 'world' as test_regular_string"}`)) /** Runnable query: @@ -110,7 +110,7 @@ type moreLiteralsQuery = { result: moreLiteralsResult, } -%%private(let moreLiteralsIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n 'success' as status,\n 'error' as error_status,\n 'pending' as pending_status,\n 42 as magic_number,\n 0 as zero_value,\n -1 as negative_one,\n 100 as max_percentage,\n 'admin' as admin_role,\n 'user' as user_role,\n 'guest' as guest_role"}`)) +%%private(let moreLiteralsIR: IR.t = %raw(`{"queryName":"MoreLiterals","usedParamSet":{},"params":[],"statement":"select\n 'success' as status,\n 'error' as error_status,\n 'pending' as pending_status,\n 42 as magic_number,\n 0 as zero_value,\n -1 as negative_one,\n 100 as max_percentage,\n 'admin' as admin_role,\n 'user' as user_role,\n 'guest' as guest_role"}`)) /** Runnable query: @@ -199,7 +199,7 @@ type duplicateAliasTestQuery = { result: duplicateAliasTestResult, } -%%private(let duplicateAliasTestIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n 'main' as status,\n 1 as priority,\n (select 'sub' as status) as sub_status"}`)) +%%private(let duplicateAliasTestIR: IR.t = %raw(`{"queryName":"DuplicateAliasTest","usedParamSet":{},"params":[],"statement":"select\n 'main' as status,\n 1 as priority,\n (select 'sub' as status) as sub_status"}`)) /** Runnable query: @@ -280,7 +280,7 @@ type unionTestQuery = { result: unionTestResult, } -%%private(let unionTestIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select 'draft' as document_status, 1 as version\n union all\n select 'published' as document_status, 2 as version"}`)) +%%private(let unionTestIR: IR.t = %raw(`{"queryName":"UnionTest","usedParamSet":{},"params":[],"statement":"select 'draft' as document_status, 1 as version\n union all\n select 'published' as document_status, 2 as version"}`)) /** Runnable query: @@ -360,7 +360,7 @@ type unionTestWithStringQuery = { result: unionTestWithStringResult, } -%%private(let unionTestWithStringIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select 'draft' as document_status, 1 as version\n union all\n select 'published' as document_status, 2 as version\n union all\n select 'draft' || 'two' as document_status, 3 as version"}`)) +%%private(let unionTestWithStringIR: IR.t = %raw(`{"queryName":"UnionTestWithString","usedParamSet":{},"params":[],"statement":"select 'draft' as document_status, 1 as version\n union all\n select 'published' as document_status, 2 as version\n union all\n select 'draft' || 'two' as document_status, 3 as version"}`)) /** Runnable query: @@ -443,7 +443,7 @@ type singleLiteralsQuery = { result: singleLiteralsResult, } -%%private(let singleLiteralsIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n 'confirmed' as booking_status,\n 5 as rating_stars,\n 'premium' as subscription_tier"}`)) +%%private(let singleLiteralsIR: IR.t = %raw(`{"queryName":"SingleLiterals","usedParamSet":{},"params":[],"statement":"select\n 'confirmed' as booking_status,\n 5 as rating_stars,\n 'premium' as subscription_tier"}`)) /** Runnable query: @@ -530,7 +530,7 @@ type edgeCasesQuery = { result: edgeCasesResult, } -%%private(let edgeCasesIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select\n '' as empty_string,\n -999 as negative_number,\n 0 as zero,\n 'null' as null_string,\n 123456789 as large_number,\n 'a' as single_char,\n 'with spaces' as string_with_spaces,\n -1 as minus_one"}`)) +%%private(let edgeCasesIR: IR.t = %raw(`{"queryName":"EdgeCases","usedParamSet":{},"params":[],"statement":"select\n '' as empty_string,\n -999 as negative_number,\n 0 as zero,\n 'null' as null_string,\n 123456789 as large_number,\n 'a' as single_char,\n 'with spaces' as string_with_spaces,\n -1 as minus_one"}`)) /** Runnable query: @@ -617,7 +617,7 @@ type contextTestQuery = { result: contextTestResult, } -%%private(let contextTestIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"select \n 'outer_result' as status,\n 'final_value' as result_type,\n (\n select count(*)\n from (\n select 'inner_result' as status, -- Same alias but different context\n 'internal_value' as result_type -- Same alias but different context\n from generate_series(1,3)\n ) inner_table\n where inner_table.status = 'inner_result' -- This literal is used for filtering\n ) as nested_count"}`)) +%%private(let contextTestIR: IR.t = %raw(`{"queryName":"ContextTest","usedParamSet":{},"params":[],"statement":"select \n 'outer_result' as status,\n 'final_value' as result_type,\n (\n select count(*)\n from (\n select 'inner_result' as status, -- Same alias but different context\n 'internal_value' as result_type -- Same alias but different context\n from generate_series(1,3)\n ) inner_table\n where inner_table.status = 'inner_result' -- This literal is used for filtering\n ) as nested_count"}`)) /** Runnable query: diff --git a/packages/example/src/books/books__sql.res b/packages/example/src/books/books__sql.res index f9947a61..d6a80ed9 100644 --- a/packages/example/src/books/books__sql.res +++ b/packages/example/src/books/books__sql.res @@ -40,7 +40,7 @@ type findBookByIdQuery = { result: findBookByIdResult, } -%%private(let findBookByIdIR: IR.t = %raw(`{"usedParamSet":{"id":true},"params":[{"name":"id","required":false,"transform":{"type":"scalar"},"locs":[{"a":31,"b":33}]}],"statement":"SELECT * FROM books WHERE id = :id"}`)) +%%private(let findBookByIdIR: IR.t = %raw(`{"queryName":"FindBookById","usedParamSet":{"id":true},"params":[{"name":"id","required":false,"transform":{"type":"scalar"},"locs":[{"a":31,"b":33}]}],"statement":"SELECT * FROM books WHERE id = :id"}`)) /** Runnable query: @@ -123,7 +123,7 @@ type findBookByCategoryQuery = { result: findBookByCategoryResult, } -%%private(let findBookByCategoryIR: IR.t = %raw(`{"usedParamSet":{"category":true},"params":[{"name":"category","required":false,"transform":{"type":"scalar"},"locs":[{"a":26,"b":34}]}],"statement":"SELECT * FROM books WHERE :category = ANY(categories)"}`)) +%%private(let findBookByCategoryIR: IR.t = %raw(`{"queryName":"FindBookByCategory","usedParamSet":{"category":true},"params":[{"name":"category","required":false,"transform":{"type":"scalar"},"locs":[{"a":26,"b":34}]}],"statement":"SELECT * FROM books WHERE :category = ANY(categories)"}`)) /** Runnable query: @@ -204,7 +204,7 @@ type findBookNameOrRankQuery = { result: findBookNameOrRankResult, } -%%private(let findBookNameOrRankIR: IR.t = %raw(`{"usedParamSet":{"name":true,"rank":true},"params":[{"name":"name","required":false,"transform":{"type":"scalar"},"locs":[{"a":41,"b":45}]},{"name":"rank","required":false,"transform":{"type":"scalar"},"locs":[{"a":57,"b":61}]}],"statement":"SELECT id, name\nFROM books\nWHERE (name = :name OR rank = :rank)"}`)) +%%private(let findBookNameOrRankIR: IR.t = %raw(`{"queryName":"FindBookNameOrRank","usedParamSet":{"name":true,"rank":true},"params":[{"name":"name","required":false,"transform":{"type":"scalar"},"locs":[{"a":41,"b":45}]},{"name":"rank","required":false,"transform":{"type":"scalar"},"locs":[{"a":57,"b":61}]}],"statement":"SELECT id, name\nFROM books\nWHERE (name = :name OR rank = :rank)"}`)) /** Runnable query: @@ -287,7 +287,7 @@ type findBookUnicodeQuery = { result: findBookUnicodeResult, } -%%private(let findBookUnicodeIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT * FROM books WHERE name = 'שקל'"}`)) +%%private(let findBookUnicodeIR: IR.t = %raw(`{"queryName":"FindBookUnicode","usedParamSet":{},"params":[],"statement":"SELECT * FROM books WHERE name = 'שקל'"}`)) /** Runnable query: @@ -373,7 +373,7 @@ type insertBooksQuery = { result: insertBooksResult, } -%%private(let insertBooksIR: IR.t = %raw(`{"usedParamSet":{"books":true},"params":[{"name":"books","required":false,"transform":{"type":"pick_array_spread","keys":[{"name":"rank","required":true},{"name":"name","required":true},{"name":"authorId","required":true},{"name":"categories","required":false}]},"locs":[{"a":61,"b":66}]}],"statement":"INSERT INTO books (rank, name, author_id, categories)\nVALUES :books RETURNING id as book_id"}`)) +%%private(let insertBooksIR: IR.t = %raw(`{"queryName":"InsertBooks","usedParamSet":{"books":true},"params":[{"name":"books","required":false,"transform":{"type":"pick_array_spread","keys":[{"name":"rank","required":true},{"name":"name","required":true},{"name":"authorId","required":true},{"name":"categories","required":false}]},"locs":[{"a":61,"b":66}]}],"statement":"INSERT INTO books (rank, name, author_id, categories)\nVALUES :books RETURNING id as book_id"}`)) /** Runnable query: @@ -456,7 +456,7 @@ type insertBookQuery = { result: insertBookResult, } -%%private(let insertBookIR: IR.t = %raw(`{"usedParamSet":{"rank":true,"name":true,"author_id":true,"categories":true},"params":[{"name":"rank","required":true,"transform":{"type":"scalar"},"locs":[{"a":62,"b":67}]},{"name":"name","required":true,"transform":{"type":"scalar"},"locs":[{"a":70,"b":75}]},{"name":"author_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":78,"b":88}]},{"name":"categories","required":false,"transform":{"type":"scalar"},"locs":[{"a":91,"b":101}]}],"statement":"INSERT INTO books (rank, name, author_id, categories)\nVALUES (:rank!, :name!, :author_id!, :categories) RETURNING id as book_id"}`)) +%%private(let insertBookIR: IR.t = %raw(`{"queryName":"InsertBook","usedParamSet":{"rank":true,"name":true,"author_id":true,"categories":true},"params":[{"name":"rank","required":true,"transform":{"type":"scalar"},"locs":[{"a":62,"b":67}]},{"name":"name","required":true,"transform":{"type":"scalar"},"locs":[{"a":70,"b":75}]},{"name":"author_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":78,"b":88}]},{"name":"categories","required":false,"transform":{"type":"scalar"},"locs":[{"a":91,"b":101}]}],"statement":"INSERT INTO books (rank, name, author_id, categories)\nVALUES (:rank!, :name!, :author_id!, :categories) RETURNING id as book_id"}`)) /** Runnable query: @@ -535,7 +535,7 @@ type updateBooksCustomQuery = { result: updateBooksCustomResult, } -%%private(let updateBooksCustomIR: IR.t = %raw(`{"usedParamSet":{"rank":true,"id":true},"params":[{"name":"rank","required":false,"transform":{"type":"scalar"},"locs":[{"a":49,"b":53},{"a":95,"b":99}]},{"name":"id","required":true,"transform":{"type":"scalar"},"locs":[{"a":161,"b":164}]}],"statement":"UPDATE books\nSET\n rank = (\n CASE WHEN (:rank::int IS NOT NULL)\n THEN :rank\n ELSE rank\n END\n )\nWHERE id = :id!"}`)) +%%private(let updateBooksCustomIR: IR.t = %raw(`{"queryName":"UpdateBooksCustom","usedParamSet":{"rank":true,"id":true},"params":[{"name":"rank","required":false,"transform":{"type":"scalar"},"locs":[{"a":49,"b":53},{"a":95,"b":99}]},{"name":"id","required":true,"transform":{"type":"scalar"},"locs":[{"a":161,"b":164}]}],"statement":"UPDATE books\nSET\n rank = (\n CASE WHEN (:rank::int IS NOT NULL)\n THEN :rank\n ELSE rank\n END\n )\nWHERE id = :id!"}`)) /** Runnable query: @@ -622,7 +622,7 @@ type updateBooksQuery = { result: updateBooksResult, } -%%private(let updateBooksIR: IR.t = %raw(`{"usedParamSet":{"name":true,"rank":true,"id":true},"params":[{"name":"name","required":false,"transform":{"type":"scalar"},"locs":[{"a":50,"b":54}]},{"name":"rank","required":false,"transform":{"type":"scalar"},"locs":[{"a":68,"b":72}]},{"name":"id","required":true,"transform":{"type":"scalar"},"locs":[{"a":85,"b":88}]}],"statement":"UPDATE books\n \nSET\n name = :name,\n rank = :rank\nWHERE id = :id!"}`)) +%%private(let updateBooksIR: IR.t = %raw(`{"queryName":"UpdateBooks","usedParamSet":{"name":true,"rank":true,"id":true},"params":[{"name":"name","required":false,"transform":{"type":"scalar"},"locs":[{"a":50,"b":54}]},{"name":"rank","required":false,"transform":{"type":"scalar"},"locs":[{"a":68,"b":72}]},{"name":"id","required":true,"transform":{"type":"scalar"},"locs":[{"a":85,"b":88}]}],"statement":"UPDATE books\n \nSET\n name = :name,\n rank = :rank\nWHERE id = :id!"}`)) /** Runnable query: @@ -706,7 +706,7 @@ type updateBooksRankNotNullQuery = { result: updateBooksRankNotNullResult, } -%%private(let updateBooksRankNotNullIR: IR.t = %raw(`{"usedParamSet":{"rank":true,"name":true,"id":true},"params":[{"name":"rank","required":true,"transform":{"type":"scalar"},"locs":[{"a":28,"b":33}]},{"name":"name","required":false,"transform":{"type":"scalar"},"locs":[{"a":47,"b":51}]},{"name":"id","required":true,"transform":{"type":"scalar"},"locs":[{"a":64,"b":67}]}],"statement":"UPDATE books\nSET\n rank = :rank!,\n name = :name\nWHERE id = :id!"}`)) +%%private(let updateBooksRankNotNullIR: IR.t = %raw(`{"queryName":"UpdateBooksRankNotNull","usedParamSet":{"rank":true,"name":true,"id":true},"params":[{"name":"rank","required":true,"transform":{"type":"scalar"},"locs":[{"a":28,"b":33}]},{"name":"name","required":false,"transform":{"type":"scalar"},"locs":[{"a":47,"b":51}]},{"name":"id","required":true,"transform":{"type":"scalar"},"locs":[{"a":64,"b":67}]}],"statement":"UPDATE books\nSET\n rank = :rank!,\n name = :name\nWHERE id = :id!"}`)) /** Runnable query: @@ -793,7 +793,7 @@ type getBooksByAuthorNameQuery = { result: getBooksByAuthorNameResult, } -%%private(let getBooksByAuthorNameIR: IR.t = %raw(`{"usedParamSet":{"authorName":true},"params":[{"name":"authorName","required":true,"transform":{"type":"scalar"},"locs":[{"a":110,"b":121}]}],"statement":"SELECT b.* FROM books b\nINNER JOIN authors a ON a.id = b.author_id\nWHERE a.first_name || ' ' || a.last_name = :authorName!"}`)) +%%private(let getBooksByAuthorNameIR: IR.t = %raw(`{"queryName":"GetBooksByAuthorName","usedParamSet":{"authorName":true},"params":[{"name":"authorName","required":true,"transform":{"type":"scalar"},"locs":[{"a":110,"b":121}]}],"statement":"SELECT b.* FROM books b\nINNER JOIN authors a ON a.id = b.author_id\nWHERE a.first_name || ' ' || a.last_name = :authorName!"}`)) /** Runnable query: @@ -875,7 +875,7 @@ type aggregateEmailsAndTestQuery = { result: aggregateEmailsAndTestResult, } -%%private(let aggregateEmailsAndTestIR: IR.t = %raw(`{"usedParamSet":{"testAges":true},"params":[{"name":"testAges","required":false,"transform":{"type":"scalar"},"locs":[{"a":55,"b":63}]}],"statement":"SELECT array_agg(email) as \"emails!\", array_agg(age) = :testAges as ageTest FROM users"}`)) +%%private(let aggregateEmailsAndTestIR: IR.t = %raw(`{"queryName":"AggregateEmailsAndTest","usedParamSet":{"testAges":true},"params":[{"name":"testAges","required":false,"transform":{"type":"scalar"},"locs":[{"a":55,"b":63}]}],"statement":"SELECT array_agg(email) as \"emails!\", array_agg(age) = :testAges as ageTest FROM users"}`)) /** Runnable query: @@ -953,7 +953,7 @@ type getBooksQuery = { result: getBooksResult, } -%%private(let getBooksIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT id, name as \"name!\" FROM books"}`)) +%%private(let getBooksIR: IR.t = %raw(`{"queryName":"GetBooks","usedParamSet":{},"params":[],"statement":"SELECT id, name as \"name!\" FROM books"}`)) /** Runnable query: @@ -1030,7 +1030,7 @@ type countBooksQuery = { result: countBooksResult, } -%%private(let countBooksIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT count(*) as book_count FROM books"}`)) +%%private(let countBooksIR: IR.t = %raw(`{"queryName":"CountBooks","usedParamSet":{},"params":[],"statement":"SELECT count(*) as book_count FROM books"}`)) /** Runnable query: @@ -1108,7 +1108,7 @@ type getBookCountriesQuery = { result: getBookCountriesResult, } -%%private(let getBookCountriesIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT * FROM book_country"}`)) +%%private(let getBookCountriesIR: IR.t = %raw(`{"queryName":"GetBookCountries","usedParamSet":{},"params":[],"statement":"SELECT * FROM book_country"}`)) /** Runnable query: diff --git a/packages/example/src/comments/comments__sql.res b/packages/example/src/comments/comments__sql.res index 9543d4c9..0fc5ddfd 100644 --- a/packages/example/src/comments/comments__sql.res +++ b/packages/example/src/comments/comments__sql.res @@ -24,7 +24,7 @@ type getAllCommentsQuery = { result: getAllCommentsResult, } -%%private(let getAllCommentsIR: IR.t = %raw(`{"usedParamSet":{"id":true},"params":[{"name":"id","required":true,"transform":{"type":"scalar"},"locs":[{"a":39,"b":42},{"a":57,"b":59}]}],"statement":"SELECT * FROM book_comments WHERE id = :id! OR user_id = :id "}`)) +%%private(let getAllCommentsIR: IR.t = %raw(`{"queryName":"GetAllComments","usedParamSet":{"id":true},"params":[{"name":"id","required":true,"transform":{"type":"scalar"},"locs":[{"a":39,"b":42},{"a":57,"b":59}]}],"statement":"SELECT * FROM book_comments WHERE id = :id! OR user_id = :id "}`)) /** Runnable query: @@ -106,7 +106,7 @@ type getAllCommentsByIdsQuery = { result: getAllCommentsByIdsResult, } -%%private(let getAllCommentsByIdsIR: IR.t = %raw(`{"usedParamSet":{"ids":true},"params":[{"name":"ids","required":true,"transform":{"type":"array_spread"},"locs":[{"a":40,"b":43},{"a":55,"b":59}]}],"statement":"SELECT * FROM book_comments WHERE id in :ids AND id in :ids!"}`)) +%%private(let getAllCommentsByIdsIR: IR.t = %raw(`{"queryName":"GetAllCommentsByIds","usedParamSet":{"ids":true},"params":[{"name":"ids","required":true,"transform":{"type":"array_spread"},"locs":[{"a":40,"b":43},{"a":55,"b":59}]}],"statement":"SELECT * FROM book_comments WHERE id in :ids AND id in :ids!"}`)) /** Runnable query: @@ -193,7 +193,7 @@ type insertCommentQuery = { result: insertCommentResult, } -%%private(let insertCommentIR: IR.t = %raw(`{"usedParamSet":{"comments":true},"params":[{"name":"comments","required":false,"transform":{"type":"pick_array_spread","keys":[{"name":"userId","required":true},{"name":"commentBody","required":true}]},"locs":[{"a":73,"b":81}]}],"statement":"INSERT INTO book_comments (user_id, body)\n-- NOTE: this is a note\nVALUES :comments RETURNING *"}`)) +%%private(let insertCommentIR: IR.t = %raw(`{"queryName":"InsertComment","usedParamSet":{"comments":true},"params":[{"name":"comments","required":false,"transform":{"type":"pick_array_spread","keys":[{"name":"userId","required":true},{"name":"commentBody","required":true}]},"locs":[{"a":73,"b":81}]}],"statement":"INSERT INTO book_comments (user_id, body)\n-- NOTE: this is a note\nVALUES :comments RETURNING *"}`)) /** Runnable query: @@ -272,7 +272,7 @@ type selectExistsTestQuery = { result: selectExistsTestResult, } -%%private(let selectExistsTestIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT EXISTS ( SELECT 1 WHERE true ) AS \"isTransactionExists\""}`)) +%%private(let selectExistsTestIR: IR.t = %raw(`{"queryName":"SelectExistsTest","usedParamSet":{},"params":[],"statement":"SELECT EXISTS ( SELECT 1 WHERE true ) AS \"isTransactionExists\""}`)) /** Runnable query: diff --git a/packages/example/src/notifications/notifications__sql.res b/packages/example/src/notifications/notifications__sql.res index 09f480e8..074cac55 100644 --- a/packages/example/src/notifications/notifications__sql.res +++ b/packages/example/src/notifications/notifications__sql.res @@ -30,7 +30,7 @@ type sendNotificationsQuery = { result: sendNotificationsResult, } -%%private(let sendNotificationsIR: IR.t = %raw(`{"usedParamSet":{"notifications":true},"params":[{"name":"notifications","required":false,"transform":{"type":"pick_array_spread","keys":[{"name":"user_id","required":true},{"name":"payload","required":true},{"name":"type","required":true}]},"locs":[{"a":58,"b":71}]}],"statement":"INSERT INTO notifications (user_id, payload, type)\nVALUES :notifications RETURNING id as notification_id"}`)) +%%private(let sendNotificationsIR: IR.t = %raw(`{"queryName":"SendNotifications","usedParamSet":{"notifications":true},"params":[{"name":"notifications","required":false,"transform":{"type":"pick_array_spread","keys":[{"name":"user_id","required":true},{"name":"payload","required":true},{"name":"type","required":true}]},"locs":[{"a":58,"b":71}]}],"statement":"INSERT INTO notifications (user_id, payload, type)\nVALUES :notifications RETURNING id as notification_id"}`)) /** Runnable query: @@ -115,7 +115,7 @@ type getNotificationsQuery = { result: getNotificationsResult, } -%%private(let getNotificationsIR: IR.t = %raw(`{"usedParamSet":{"userId":true,"date":true},"params":[{"name":"userId","required":false,"transform":{"type":"scalar"},"locs":[{"a":47,"b":53}]},{"name":"date","required":true,"transform":{"type":"scalar"},"locs":[{"a":73,"b":78}]}],"statement":"SELECT *\n FROM notifications\n WHERE user_id = :userId\n AND created_at > :date!"}`)) +%%private(let getNotificationsIR: IR.t = %raw(`{"queryName":"GetNotifications","usedParamSet":{"userId":true,"date":true},"params":[{"name":"userId","required":false,"transform":{"type":"scalar"},"locs":[{"a":47,"b":53}]},{"name":"date","required":true,"transform":{"type":"scalar"},"locs":[{"a":73,"b":78}]}],"statement":"SELECT *\n FROM notifications\n WHERE user_id = :userId\n AND created_at > :date!"}`)) /** Runnable query: @@ -199,7 +199,7 @@ type thresholdFrogsQuery = { result: thresholdFrogsResult, } -%%private(let thresholdFrogsIR: IR.t = %raw(`{"usedParamSet":{"numFrogs":true},"params":[{"name":"numFrogs","required":true,"transform":{"type":"scalar"},"locs":[{"a":143,"b":152}]}],"statement":"SELECT u.user_name, n.payload, n.type\nFROM notifications n\nINNER JOIN users u on n.user_id = u.id\nWHERE CAST (n.payload->'num_frogs' AS int) > :numFrogs!"}`)) +%%private(let thresholdFrogsIR: IR.t = %raw(`{"queryName":"ThresholdFrogs","usedParamSet":{"numFrogs":true},"params":[{"name":"numFrogs","required":true,"transform":{"type":"scalar"},"locs":[{"a":143,"b":152}]}],"statement":"SELECT u.user_name, n.payload, n.type\nFROM notifications n\nINNER JOIN users u on n.user_id = u.id\nWHERE CAST (n.payload->'num_frogs' AS int) > :numFrogs!"}`)) /** Runnable query: diff --git a/packages/parser/src/loader/sql/index.ts b/packages/parser/src/loader/sql/index.ts index 0d541744..ca2c6d02 100644 --- a/packages/parser/src/loader/sql/index.ts +++ b/packages/parser/src/loader/sql/index.ts @@ -84,6 +84,7 @@ export interface QueryIR { params: ParamIR[]; statement: string; usedParamSet: QueryAST['usedParamSet']; + queryName: string; } interface ParseTree { @@ -295,6 +296,7 @@ export function queryASTToIR(query: SQLQueryAST): SQLQueryIR { const { a: statementStart } = query.statement.loc; return { + queryName: query.name, usedParamSet: query.usedParamSet, params: query.params.map((param) => ({ name: param.name, diff --git a/packages/runtime/src/preprocessor-sql.test.ts b/packages/runtime/src/preprocessor-sql.test.ts index 770f7059..676293cd 100644 --- a/packages/runtime/src/preprocessor-sql.test.ts +++ b/packages/runtime/src/preprocessor-sql.test.ts @@ -1,6 +1,10 @@ import { queryASTToIR, parseSQLFile as parseSQLQuery } from '@pgtyped/parser'; import { processSQLQueryIR } from './preprocessor-sql.js'; -import { ParameterTransform } from './preprocessor.js'; +import { + InterpolatedQuery, + ParameterTransform, + QueryParameter, +} from './preprocessor.js'; test('(SQL) no params', () => { const query = ` @@ -14,6 +18,7 @@ test('(SQL) no params', () => { query: 'SELECT id, name FROM users', mapping: [], bindings: [], + name: 'selectSomeUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -42,11 +47,12 @@ test('(SQL) two scalar params, one forced as non-null', () => { id: 'id', }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'UPDATE books\n SET\n rank = $1,\n name = $2\n WHERE id = $3', mapping: [], bindings: [123, 'name', 'id'], + name: 'UpdateBooksRankNotNull', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -66,10 +72,11 @@ test('(SQL) two scalar params', () => { age: 12, }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'SELECT id, name from users where id = $1 and age > $2', mapping: [], bindings: ['123', 12], + name: 'selectSomeUsers', }; const expectedMappingResult = { @@ -89,6 +96,7 @@ test('(SQL) two scalar params', () => { }, ], bindings: [], + name: 'selectSomeUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -109,10 +117,11 @@ test('(SQL) one param used twice', () => { id: '123', }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'SELECT id, name from users where id = $1 or parent_id = $1', mapping: [], bindings: ['123'], + name: 'selectUsersAndParents', }; const expectedMappingResult = { @@ -126,6 +135,7 @@ test('(SQL) one param used twice', () => { }, ], bindings: [], + name: 'selectUsersAndParents', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -149,13 +159,14 @@ test('(SQL) array param', () => { ages: [23, 27, 50], }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'SELECT FROM users WHERE age in ($1,$2,$3)', bindings: [23, 27, 50], mapping: [], + name: 'selectSomeUsers', }; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'SELECT FROM users WHERE age in ($1)', bindings: [], mapping: [ @@ -166,6 +177,7 @@ test('(SQL) array param', () => { assignedIndex: 1, }, ], + name: 'selectSomeUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -189,13 +201,14 @@ test('(SQL) array param used twice', () => { ages: [23, 27, 50], }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'SELECT FROM users WHERE age in ($1,$2,$3) or age in ($1,$2,$3)', bindings: [23, 27, 50], mapping: [], + name: 'selectSomeUsers', }; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'SELECT FROM users WHERE age in ($1) or age in ($1)', bindings: [], mapping: [ @@ -206,6 +219,7 @@ test('(SQL) array param used twice', () => { assignedIndex: 1, }, ], + name: 'selectSomeUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -230,13 +244,14 @@ test('(SQL) array and scalar param', () => { userId: 'some-id', }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'SELECT FROM users WHERE age in ($1,$2,$3) and id = $4', bindings: [23, 27, 50, 'some-id'], mapping: [], + name: 'selectSomeUsers', }; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'SELECT FROM users WHERE age in ($1) and id = $2', bindings: [], mapping: [ @@ -253,6 +268,7 @@ test('(SQL) array and scalar param', () => { assignedIndex: 2, }, ], + name: 'selectSomeUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -276,13 +292,14 @@ test('(SQL) pick param', () => { user: { name: 'Bob', age: 12 }, }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2) RETURNING id', bindings: ['Bob', 12], mapping: [], + name: 'insertUsers', }; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2) RETURNING id', bindings: [], mapping: [ @@ -305,6 +322,7 @@ test('(SQL) pick param', () => { }, }, ], + name: 'insertUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -328,13 +346,14 @@ test('(SQL) pick param used twice', () => { user: { name: 'Bob', age: 12 }, }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2), ($1,$2) RETURNING id', bindings: ['Bob', 12], mapping: [], + name: 'insertUsersTwice', }; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2), ($1,$2) RETURNING id', bindings: [], mapping: [ @@ -357,6 +376,7 @@ test('(SQL) pick param used twice', () => { }, }, ], + name: 'insertUsersTwice', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -383,10 +403,11 @@ test('(SQL) pickSpread param', () => { ], }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2),($3,$4) RETURNING id', bindings: ['Bob', 12, 'Tom', 22], mapping: [], + name: 'insertUsers', }; const expectedMapping = [ @@ -414,6 +435,7 @@ test('(SQL) pickSpread param', () => { query: 'INSERT INTO users (name, age) VALUES ($1,$2) RETURNING id', bindings: [], mapping: expectedMapping, + name: 'insertUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -440,14 +462,15 @@ test('(SQL) pickSpread param used twice', () => { ], }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2),($3,$4), ($1,$2),($3,$4) RETURNING id', bindings: ['Bob', 12, 'Tom', 22], mapping: [], + name: 'insertUsers', }; - const expectedMapping = [ + const expectedMapping: QueryParameter[] = [ { name: 'users', type: ParameterTransform.PickSpread, @@ -468,10 +491,11 @@ test('(SQL) pickSpread param used twice', () => { }, ]; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2), ($1,$2) RETURNING id', bindings: [], mapping: expectedMapping, + name: 'insertUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -492,13 +516,14 @@ test('(SQL) scalar param required and optional', () => { id: '123', }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'SELECT id, name from users where id = $1 and user_id = $1', mapping: [], bindings: ['123'], + name: 'selectSomeUsers', }; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'SELECT id, name from users where id = $1 and user_id = $1', mapping: [ { @@ -509,6 +534,7 @@ test('(SQL) scalar param required and optional', () => { }, ], bindings: [], + name: 'selectSomeUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -532,13 +558,14 @@ test('(SQL) pick param required', () => { user: { name: 'Bob', age: 12 }, }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2) RETURNING id', bindings: ['Bob', 12], mapping: [], + name: 'insertUsers', }; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1,$2) RETURNING id', bindings: [], mapping: [ @@ -561,6 +588,7 @@ test('(SQL) pick param required', () => { }, }, ], + name: 'insertUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); @@ -584,13 +612,14 @@ test('(SQL) array param required', () => { ages: [23, 27, 50], }; - const expectedInterpolationResult = { + const expectedInterpolationResult: InterpolatedQuery = { query: 'SELECT FROM users WHERE age in ($1,$2,$3)', bindings: [23, 27, 50], mapping: [], + name: 'selectSomeUsers', }; - const expectedMappingResult = { + const expectedMappingResult: InterpolatedQuery = { query: 'SELECT FROM users WHERE age in ($1)', bindings: [], mapping: [ @@ -601,6 +630,7 @@ test('(SQL) array param required', () => { assignedIndex: 1, }, ], + name: 'selectSomeUsers', }; const queryIR = queryASTToIR(fileAST.queries[0]); diff --git a/packages/runtime/src/preprocessor-sql.ts b/packages/runtime/src/preprocessor-sql.ts index 6bb051d9..096eac73 100644 --- a/packages/runtime/src/preprocessor-sql.ts +++ b/packages/runtime/src/preprocessor-sql.ts @@ -14,7 +14,6 @@ import { /* Processes query AST formed by new parser from pure SQL files */ export const processSQLQueryIR = ( queryIR: SQLQueryIR, - queryName: string | undefined, passedParams?: QueryParameters, ): InterpolatedQuery => { const bindings: Scalar[] = []; @@ -170,6 +169,6 @@ export const processSQLQueryIR = ( mapping: paramMapping, query: flatStr, bindings, - name: queryName, + name: queryIR.queryName, }; }; diff --git a/packages/runtime/src/preprocessor-ts.test.ts b/packages/runtime/src/preprocessor-ts.test.ts index 2a46a104..6e933336 100644 --- a/packages/runtime/src/preprocessor-ts.test.ts +++ b/packages/runtime/src/preprocessor-ts.test.ts @@ -1,5 +1,5 @@ import { parseTSQuery } from '@pgtyped/parser'; -import { ParameterTransform } from './preprocessor.js'; +import { InterpolatedQuery, ParameterTransform } from './preprocessor.js'; import { processTSQueryAST } from './preprocessor-ts.js'; test('(TS) name parameter interpolation', () => { @@ -10,10 +10,11 @@ test('(TS) name parameter interpolation', () => { age: 12, }; - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'SELECT id, name from users where id = $1 and age > $2', mapping: [], bindings: ['123', 12], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, parameters); @@ -41,6 +42,7 @@ test('(TS) pick parameter interpolation (multiline)', () => { VALUES ($1, $2, $3)`, mapping: [], bindings: [{ num_frogs: 1002 }, 1, 'reminder'], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, parameters as any); @@ -70,6 +72,7 @@ test('(TS) pick array parameter interpolation (multiline)', () => { VALUES ($1, $2, $3)`, mapping: [], bindings: [{ num_frogs: 1002 }, 1, 'reminder'], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, parameters as any); @@ -84,10 +87,11 @@ test('(TS) scalar param used twice', () => { id: '123', }; - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'SELECT id, name from users where id = $1 and parent_id = $1', mapping: [], bindings: ['123'], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, parameters); @@ -100,11 +104,12 @@ test('(TS) name parameter mapping', () => { 'SELECT id, name from users where id = $id and age > $age and parent_id = $id'; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'SELECT id, name from users where id = $1 and age > $2 and parent_id = $1', mapping: [], bindings: ['1234-1235', 33], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, { @@ -120,7 +125,7 @@ test('(TS) single value list parameter interpolation', () => { 'INSERT INTO users (name, age) VALUES $user(name, age) RETURNING id'; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id', mapping: [ { @@ -143,6 +148,7 @@ test('(TS) single value list parameter interpolation', () => { }, ], bindings: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query); @@ -163,11 +169,12 @@ test('(TS) single value list parameter interpolation twice', () => { }, }; - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1, $2) BOGUS ($1, $3) RETURNING id', mapping: [], bindings: ['Bob', 12, '1234-123-1233'], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, parameters); @@ -180,7 +187,7 @@ test('(TS) multiple value list (array) parameter mapping', () => { 'SELECT FROM users where (age in $$ages and age in $$ages) or (age in $$otherAges)'; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'SELECT FROM users where (age in ($1) and age in ($1)) or (age in ($2))', mapping: [ @@ -198,6 +205,7 @@ test('(TS) multiple value list (array) parameter mapping', () => { }, ], bindings: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query); @@ -213,11 +221,12 @@ test('(TS) multiple value list (array) parameter interpolation', () => { ages: [23, 27, 50], }; - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'SELECT FROM users where age in ($1, $2, $3) or parent_age in ($1, $2, $3)', bindings: [23, 27, 50], mapping: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, parameters); @@ -230,7 +239,7 @@ test('(TS) multiple value list parameter mapping', () => { 'INSERT INTO users (name, age) VALUES $$users(name, age) RETURNING id'; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id', bindings: [], mapping: [ @@ -253,6 +262,7 @@ test('(TS) multiple value list parameter mapping', () => { }, }, ], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query); @@ -265,7 +275,7 @@ test('(TS) multiple value list parameter mapping twice', () => { 'INSERT INTO users (name, age) VALUES $$users(name, age), $$users(name) RETURNING id'; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1, $2), ($1) RETURNING id', bindings: [], mapping: [ @@ -288,6 +298,7 @@ test('(TS) multiple value list parameter mapping twice', () => { }, }, ], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query); @@ -307,11 +318,12 @@ test('(TS) multiple value list parameter interpolation', () => { ], }; - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1, $2), ($3, $4) RETURNING id', bindings: ['Bob', 12, 'Tom', 22], mapping: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, parameters); @@ -331,11 +343,12 @@ test('(TS) multiple value list parameter interpolation twice', () => { ], }; - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'INSERT INTO users (name, age) VALUES ($1, $2), ($3, $4), ($5, $6), ($7, $8) RETURNING id', bindings: ['Bob', 12, 'Tom', 22, 'Bob', 12, 'Tom', 22], mapping: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, parameters); @@ -347,10 +360,11 @@ test('(TS) query with no params', () => { const query = `UPDATE notifications SET payload = '{"a": "b"}'::jsonb`; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: `UPDATE notifications SET payload = '{"a": "b"}'::jsonb`, bindings: [], mapping: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query); @@ -362,10 +376,11 @@ test('(TS) query with empty spread params', () => { const query = `SELECT * FROM users WHERE id IN $$ids`; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: `SELECT * FROM users WHERE id IN ()`, bindings: [], mapping: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, { ids: [] }); @@ -377,10 +392,11 @@ test('(TS) query with empty spread params', () => { const query = `INSERT INTO data.action_log (id, name) VALUES $$params(id, name)`; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: `INSERT INTO data.action_log (id, name) VALUES ()`, bindings: [], mapping: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, { params: [] }); @@ -392,10 +408,11 @@ test('(TS) query with underscores in key names and param names', () => { const query = `INSERT INTO data.action_log (_id, _name) VALUES $$_params(_id, _name)`; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: `INSERT INTO data.action_log (_id, _name) VALUES ($1, $2)`, bindings: ['one', 'two'], mapping: [], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query, { @@ -410,7 +427,7 @@ test('(TS) all kinds mapping ', () => { 'SELECT $userId $age! $userId $$users $age $user(id) $$users $user(id, parentId, age) $$comments(id!, text) $user(age!)'; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'SELECT $1 $2 $1 ($3) $2 ($4) ($3) ($4, $5, $6) ($7, $8) ($6)', bindings: [], mapping: [ @@ -475,6 +492,7 @@ test('(TS) all kinds mapping ', () => { }, }, ], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query); @@ -486,7 +504,7 @@ test('(TS) required spread', () => { const query = 'SELECT $$users!'; const parsedQuery = parseTSQuery(query); - const expectedResult = { + const expectedResult: InterpolatedQuery = { query: 'SELECT ($1)', bindings: [], mapping: [ @@ -497,6 +515,7 @@ test('(TS) required spread', () => { assignedIndex: [1], }, ], + name: 'query', }; const result = processTSQueryAST(parsedQuery.query); From 76c0f58b7f7e5f2212756ee4e8c222b581248842 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 21 Jun 2025 13:04:04 +0200 Subject: [PATCH 7/7] type the IR now that there's useful stuff in there --- packages/cli/src/res/PgTyped.res | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/res/PgTyped.res b/packages/cli/src/res/PgTyped.res index a5d71396..857d1597 100644 --- a/packages/cli/src/res/PgTyped.res +++ b/packages/cli/src/res/PgTyped.res @@ -124,7 +124,12 @@ module Pg = { } module IR = { - type t + type t = private { + queryName: option, + statement: string, + usedParamSet: dict, + params: array, // This can be more thoroughly typed if wanted + } } module PreparedStatement = {