Skip to content
Written with Claude
IMPORTANT

As you may notice, this page and pretty much the entire website were obviously created with the help of AI. I wonder how you could tell? Was it a big "Written With Claude" badge on every page? I moved it to the top now (with the help of AI of course) to make it even more obvious. There are a few blogposts that were written by me manually, the old-fashioned way, I hope there will be more in the future, and those have a similar "Human Written" badge. This project (not the website), on the other hand, is a very, very different story. It took me more than two years of painstaking and unpaid work in my own free time. A story that, hopefully, I will tell someday. But meanwhile, what would you like me to do? To create a complex documentation website with a bunch of highly technical articles with the help of AI and fake it, to give you an illusion that I also did that manually? Like the half of itnernet is doing at this point? How does that makes any sense? Is that even fair to you? Or maybe to create this website manually, the old-fashioned way, just for you? While working a paid job for a salary, most of you wouldn't even get up in the morning. Would you like me to sing you a song while we're at it? For your personal entertainment? Seriously, get a grip. Do you find this information less valuable because of the way this website was created? I give my best to fix it to keep the information as accurate as possible, and I think it is very accurate at this point. If you find some mistakes, inaccurancies or problems, there is a comment section at the bottom of every page, which I also made with the help of the AI. And I woould very much appreciate if you leave your feedback there. Look, I'm just a guy who likes SQL, that's all. If you don't approve of how this website was constructed and the use of AI tools, I suggest closing this page and never wever coming back. And good riddance. And I would ban your access if I could know how. Thank you for your attention to this matter.

PARAM

Also known as

param, parameter (with or without @ prefix)

Rename and optionally retype individual endpoint parameters. This provides better API ergonomics by replacing positional parameter names ($1, $2) or internal parameter names (_old_name) with cleaner, user-facing names.

Works on all endpoint types — functions, procedures, CRUD, and SQL file endpoints.

TIP

The @param keyword is shared with the PARAMETER_HASH annotation (@param X is hash of Y). Both forms coexist without ambiguity — the parser distinguishes them by the presence of hash of in the annotation.

Syntax

code
@param <old_name> <new_name>
@param <old_name> <new_name> <type>
@param <old_name> is <new_name>
@param <old_name> is <new_name> <type>
@param <old_name> default <value>
@param <old_name> <new_name> default <value>
@param <old_name> <new_name> <type> default <value>
@param <old_name> is <new_name> default <value>
@param <old_name> is <new_name> <type> default <value>

# `=` can be used instead of `default` in all forms above:
@param <old_name> = <value>
@param <old_name> <new_name> = <value>
@param <old_name> <new_name> <type> = <value>
@param <old_name> is <new_name> = <value>
@param <old_name> is <new_name> <type> = <value>
  • old_name: The original parameter name (e.g., $1, $2, or _old_name)
  • new_name: The new parameter name for the HTTP API. Used as-is — no name conversion is applied. If you write @param $1 authorId, the HTTP parameter name is exactly authorId, not author_id or author-id.
  • type: Optional PostgreSQL type override (e.g., integer, text, boolean)

Both @param and @parameter (long form) are supported.

Examples

Rename Positional Parameters (SQL Files)

SQL files use PostgreSQL positional parameters ($1, $2, ...) which aren't user-friendly as HTTP parameter names. Use @param to give them meaningful names:

sql
sql
-- sql/get_reports.sql
-- HTTP GET
-- @param $1 from_date
-- @param $2 to_date
select id, title, created_at
from reports
where created_at between $1 and $2;

Without rename: GET /api/get-reports?$1=2024-01-01&$2=2024-12-31

With rename: GET /api/get-reports?from_date=2024-01-01&to_date=2024-12-31

Rename with Type Override

When parameter types can't be inferred correctly, or when you need to override the inferred type:

sql
sql
-- @param $1 user_id integer
-- @param $2 active boolean
select * from users where id = $1 and active = $2;

Rename Function Parameters

Works on function and procedure parameters too — useful when internal naming conventions (like _ prefixes) shouldn't leak into the API:

sql
sql
create function get_user_profile(_user_id int, _include_stats boolean)
returns json
language sql
begin atomic;
select json_build_object('id', id, 'name', name) from users where id = _user_id;
end;

comment on function get_user_profile(int, boolean) is '
HTTP GET
@param _user_id user_id
@param _include_stats include_stats
';

Without rename: GET /api/get-user-profile?_user_id=1&_include_stats=true

With rename: GET /api/get-user-profile?user_id=1&include_stats=true

"is" Style Syntax

The is keyword is optional and provides consistency with the existing @param X is hash of Y style:

sql
sql
-- These are equivalent:
-- @param $1 user_id
-- @param $1 is user_id

-- With type override:
-- @param $1 user_id integer
-- @param $1 is user_id integer

Claim Mapping with Renamed Parameters

Renamed parameters work with user_parameters claim mapping. When you rename a positional parameter to a claim-mapped name (like _user_id or _user_name), the parameter is automatically filled from the authenticated user's claims — just like it would be for a native function parameter.

sql
sql
-- sql/get_my_profile.sql
-- @authorize
-- @user_parameters
-- @param $1 _user_id
-- @param $2 _user_name
select $1 as user_id, $2 as user_name;

GET /api/get-my-profile (authenticated as user123) → [{"userId": "user123", "userName": "user"}]

The parameters are auto-filled from claims — the client doesn't need to send them. This is especially useful for SQL file endpoints where positional parameters ($1, $2) have no inherent name for claim matching.

Default Values (SQL File Parameters)

SQL file parameters can have default values via @param. When a parameter with a default is not provided in the request, the default value is bound instead of returning 404.

This is essential for SQL files because positional parameters ($1, $2) must always be bound — unlike PostgreSQL functions where the engine applies its own defaults.

Syntax

sql
sql
-- Separate annotations (rename first, then set default):
-- @param $1 user_id
-- @param user_id default null

-- Combined rename + default on a single line:
-- @param $1 user_id default null

-- Default without rename:
-- @param $1 default 'fallback'

-- Various value types:
-- @param $1 status default 'active'     -- text (single-quoted)
-- @param $1 amount default 42           -- number
-- @param $1 enabled default true        -- boolean
-- @param $1 filter default null         -- SQL NULL (unquoted)
-- @param $1 tag default 'null'          -- literal text "null" (quoted)
-- @param $1 val default                 -- no value = NULL

Inline comments after the value are ignored. The parser consumes only the value token (or quoted string) and stops.

sql
sql

-- `=` can be used instead of `default` in all forms:
-- @param $1 user_id = null
-- @param $1 user_id integer = 42
-- @param $1 is greeting = 'hey'
-- @param my_name = 'hello'

Value Parsing Rules (SQL Conventions)

  • Unquoted null (case-insensitive) → DBNull.Value
  • Single-quoted 'text value' → string literal (supports multi-word)
  • Unquoted value → raw string (Npgsql handles type conversion via NpgsqlDbType)

Example

User identity endpoint with claim-filled parameters that fall back to NULL:

sql
sql
/* HTTP GET
@authorize
@user_parameters
@param $1 _user_id default null
@param $2 _username default null
@param $3 _email default null
*/
select $1 as user_id, $2 as username, $3 as email;

When authenticated, claims fill the parameters automatically. The defaults ensure the parameters are always bindable.

Effects on Generated Output

  • TsClient: Parameters with defaults get ? suffix in TypeScript interfaces (optional)
  • OpenAPI: Parameters with defaults are marked required: false

Rename Validation

Parameter names are validated when renaming. Invalid renames are rejected with a warning log instead of silently creating broken endpoints.

Rules:

  • Must be a valid PostgreSQL identifier: starts with letter or _, followed by letters, digits, _, or $
  • Positional parameters ($1, $2) are allowed
sql
sql
-- Valid:
-- @param $1 user_id        ✓
-- @param $1 _val$1         ✓

-- Rejected (with warning log):
-- @param $1 1bad           ✗ starts with digit
-- @param $1 my-param       ✗ invalid character (hyphen)

Composite Type Parameters (SQL Files)

When a parameter type is a known composite type, the parameter is treated as a single text value. The SQL is never rewritten — it stays exactly as written.

HTTP custom types (auto-filled from HTTP calls):

sql
sql
-- @param $1 _response example_9.exchange_rate_api
select ($1::example_9.exchange_rate_api).body;

The framework makes the HTTP call and passes the response as a composite text value automatically.

Client-sent composite types:

sql
sql
-- @param $1 data my_composite_type
select ($1::my_composite_type).field1, ($1::my_composite_type).field2;

The client sends the value as PostgreSQL composite text format: ?data=("val1","val2").

If the type in @param is not a recognized PostgreSQL type or composite type, a warning is logged and the parameter keeps its original type from Describe.

Behavior

  • The rename applies to HTTP parameter names only — the SQL still uses the original parameter name or positional reference
  • Type overrides affect how the HTTP parameter value is parsed and converted before being sent to PostgreSQL
  • For multi-command SQL files, parameter types are merged across all statements — use type override to resolve conflicts
  • The parameter must exist in the endpoint's parameter list; otherwise a warning is logged and the rename is skipped
  • Renamed parameters participate in claim mapping — renaming $1 to _user_id enables automatic user_parameters filling from the name_identifier claim

Comments