• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

egobrain / equery / 51

15 May 2026 03:56PM UTC coverage: 97.087%. Remained the same
51

push

github

egobrain
Add scripts

600 of 618 relevant lines covered (97.09%)

113.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.71
/src/pg_sql.erl
1
-module(pg_sql).
2
-moduledoc """
3
SQL expression builders.
4

5
Operators, scalar functions, aggregates, CASE, type casts. These produce
6
`qast:ast_node()` values that go inside `q:where`/`q:select`/etc.
7
closures.
8

9
Inside DSL closures, native Erlang operators (`=:=`, `>`, `andalso`,
10
`+`, …) are rewritten by `equery_pt` to corresponding `pg_sql:`
11
functions, so you rarely call these directly for primitive operations.
12

13
Use the explicit `pg_sql:` form for:
14
- Functions (`pg_sql:count/1`, `pg_sql:lower/1`, `pg_sql:date_trunc/2`, …);
15
- Operators that aren't Erlang operators (`pg_sql:like/2`, `pg_sql:'~'/2`);
16
- Aggregates and `pg_sql:filter/2`;
17
- `pg_sql:case_when/1,2`;
18
- Type casts (`pg_sql:as/2`).
19

20
JSON/JSONB live in [`qjson`](`m:qjson`).
21
""".
22

23
-include("query.hrl").
24
-include("cth.hrl").
25

26
-export([
27
         'andalso'/2,
28
         'orelse'/2,
29

30
         '=:='/2,
31
         '=/='/2,
32
         '>'/2,
33
         '>='/2,
34
         '<'/2,
35
         '=<'/2,
36
         'not'/1,
37
         'is'/2,
38
         is_null/1,
39
         is_not_null/1,
40
         is_distinct_from/2,
41
         is_not_distinct_from/2,
42

43
         '+'/2,
44
         '-'/2,
45
         '*'/2,
46
         '/'/2,
47
         'div'/2,
48
         'rem'/2,
49
         'abs'/1
50
        ]).
51

52
-export([
53
         mod/2,
54
         round/1, round/2,
55
         ceil/1,
56
         floor/1,
57
         power/2,
58
         sqrt/1,
59
         ln/1,
60
         log/1, log/2,
61
         exp/1,
62
         sign/1,
63
         random/0
64
        ]).
65

66
-export([
67
         '~'/2,
68
         '~*'/2,
69
         like/2,
70
         ilike/2
71
        ]).
72

73
-export([
74
         call/3
75
        ]).
76

77
-export([
78
         sum/1,
79
         count/1,
80
         min/1,
81
         max/1,
82
         distinct/1,
83
         array_agg/1, array_agg/2,
84
         avg/1,
85
         bool_and/1,
86
         bool_or/1,
87
         every/1,
88
         string_agg/2, string_agg/3,
89
         json_agg/1, json_agg/2,
90
         jsonb_agg/1, jsonb_agg/2,
91
         json_object_agg/2,
92
         jsonb_object_agg/2,
93
         percentile_cont/2,
94
         percentile_disc/2,
95
         mode/1,
96
         filter/2,
97
         trunc/2
98
        ]).
99

100
-export([
101
         min/2,
102
         max/2,
103
         greatest/1,
104
         least/1,
105
         row/1,
106
         row/2
107
        ]).
108

109
-export([
110
         coalesce/1,
111
         case_when/1,
112
         case_when/2,
113
         in/2,
114
         exists/1
115
        ]).
116

117
%% Date/time functions
118
-export([
119
         now/0,
120
         current_timestamp/0,
121
         current_date/0,
122
         current_time/0,
123
         date_trunc/2,
124
         extract/2,
125
         date_part/2,
126
         age/1, age/2,
127
         to_char/2,
128
         to_date/2,
129
         to_timestamp/1, to_timestamp/2
130
        ]).
131

132
%% String functions
133
-export([
134
         concat/1, concat/2,
135
         length/1,
136
         char_length/1,
137
         lower/1,
138
         upper/1,
139
         trim/1, trim/2,
140
         ltrim/1, ltrim/2,
141
         rtrim/1, rtrim/2,
142
         replace/3,
143
         split_part/3,
144
         substring/2, substring/3,
145
         strpos/2,
146
         starts_with/2,
147
         regexp_replace/3, regexp_replace/4,
148
         regexp_match/2, regexp_match/3
149
        ]).
150

151
%% Array functions
152
-export([
153
         '@>'/2,
154
         '<@'/2,
155
         '&&'/2,
156
         array/1,
157
         array_length/1, array_length/2,
158
         array_position/2,
159
         array_append/2,
160
         array_prepend/2,
161
         array_remove/2,
162
         array_replace/3,
163
         array_cat/2,
164
         unnest/1
165
        ]).
166

167
%% String / array concat
168
-export([
169
         '||'/2
170
        ]).
171

172
%% Type function
173
-export([
174
         as/2,
175
         set_type/2
176
        ]).
177

178
%% =============================================================================
179
%% Sql operations
180
%% =============================================================================
181

182
%% DSL expression: either an AST node, or any term that gets auto-wrapped
183
%% as a `$value` placeholder at SQL generation time.
184
-type expr() :: qast:ast_node() | term().
185

186
-export_type([expr/0]).
187

188
%% = Primitive =================================================================
189

190
-doc(#{group => <<"Logical">>}).
191
-doc "Logical `AND`. Short-circuits on boolean literals. See [Logical Operators](https://www.postgresql.org/docs/current/functions-logical.html).".
192
-spec 'andalso'(V, V) -> V when V :: boolean() | qast:ast_node().
193
'andalso'(true, B) -> B;
2✔
194
'andalso'(A, true) -> A;
2✔
195

196
'andalso'(false, _) -> false;
2✔
197
'andalso'(_, false) -> false;
2✔
198

199
'andalso'(A, B) ->
200
    qast:exp([qast:raw("("), A, qast:raw(" and "), B, qast:raw(")")], #{type => boolean}).
26✔
201

202
-doc(#{group => <<"Logical">>}).
203
-doc "Logical `OR`. Short-circuits on boolean literals. See [Logical Operators](https://www.postgresql.org/docs/current/functions-logical.html).".
204
-spec 'orelse'(V, V) -> V when V :: boolean() | qast:ast_node().
205
'orelse'(true, _) -> true;
2✔
206
'orelse'(_, true) -> true;
2✔
207
'orelse'(false, B) -> B;
2✔
208
'orelse'(A, false) -> A;
2✔
209
'orelse'(A, B) ->
210
    qast:exp([qast:raw("("), A, qast:raw(" or "), B, qast:raw(")")], #{type => boolean}).
20✔
211

212
-doc(#{group => <<"Logical">>}).
213
-doc "Logical `NOT`. See [Logical Operators](https://www.postgresql.org/docs/current/functions-logical.html).".
214
-spec 'not'(V) -> V when V :: boolean() | qast:ast_node().
215
'not'(A) when is_boolean(A) -> not A;
4✔
216
'not'(A) ->
217
    qast:exp([qast:raw("not "), A], #{type => boolean}).
10✔
218

219
-doc(#{group => <<"Comparison">>}).
220
-doc "`A = B`. See [Comparison Operators](https://www.postgresql.org/docs/current/functions-comparison.html).".
221
-spec '=:='(expr(), expr()) -> qast:ast_node().
222
'=:='(A, B) ->
223
    qast:exp([qast:raw("("), A, qast:raw(" = "), B, qast:raw(")")], #{type => boolean}).
124✔
224

225
-doc(#{group => <<"Comparison">>}).
226
-doc "`A <> B`. See [Comparison Operators](https://www.postgresql.org/docs/current/functions-comparison.html).".
227
-spec '=/='(expr(), expr()) -> qast:ast_node().
228
'=/='(A, B) -> 'not'('=:='(A,B)).
2✔
229

230
-doc(#{group => <<"Comparison">>}).
231
-doc "`A > B`. See [Comparison Operators](https://www.postgresql.org/docs/current/functions-comparison.html).".
232
-spec '>'(expr(), expr()) -> qast:ast_node().
233
'>'(A, B) ->
234
    qast:exp([qast:raw("("), A, qast:raw(" > "), B, qast:raw(")")], #{type => boolean}).
28✔
235

236
-doc(#{group => <<"Comparison">>}).
237
-doc "`A >= B`.".
238
-spec '>='(expr(), expr()) -> qast:ast_node().
239
'>='(A, B) ->
240
    qast:exp([qast:raw("("), A, qast:raw(" >= "), B, qast:raw(")")], #{type => boolean}).
2✔
241

242
-doc(#{group => <<"Comparison">>}).
243
-doc "`A < B`.".
244
-spec '<'(expr(), expr()) -> qast:ast_node().
245
'<'(A, B) ->
246
    qast:exp([qast:raw("("), A, qast:raw(" < "), B, qast:raw(")")], #{type => boolean}).
4✔
247

248
-doc(#{group => <<"Comparison">>}).
249
-doc "`A <= B`.".
250
-spec '=<'(expr(), expr()) -> qast:ast_node().
251
'=<'(A, B) ->
252
    qast:exp([qast:raw("("), A, qast:raw(" <= "), B, qast:raw(")")], #{type => boolean}).
2✔
253

254
-doc(#{group => <<"Comparison">>}).
255
-doc "`A IS B`. Low-level building block for `IS NULL`, `IS TRUE`, etc. See [Comparison Predicates](https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE).".
256
-spec 'is'(expr(), expr()) -> qast:ast_node().
257
is(A, B) ->
258
    qast:exp([A, qast:raw(" is "), B], #{type => boolean}).
2✔
259

260
-doc(#{group => <<"Comparison">>}).
261
-doc "`A IS NULL`. See [Comparison Predicates](https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE).".
262
-spec 'is_null'(expr()) -> qast:ast_node().
263
is_null(A) ->
264
    is(A, qast:raw("null")).
2✔
265

266
-doc(#{group => <<"Comparison">>}).
267
-doc "`A IS NOT NULL`.".
268
-spec 'is_not_null'(expr()) -> qast:ast_node().
269
is_not_null(A) ->
270
    qast:exp([A, qast:raw(" is not null")], #{type => boolean}).
2✔
271

272
-doc(#{group => <<"Comparison">>}).
273
-doc """
274
`A IS DISTINCT FROM B` — NULL-safe inequality (treats `NULL = NULL` as
275
equal). See [Comparison Predicates](https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE).
276
""".
277
-spec is_distinct_from(expr(), expr()) -> qast:ast_node().
278
is_distinct_from(A, B) ->
279
    qast:exp([A, qast:raw(" is distinct from "), B], #{type => boolean}).
4✔
280

281
-doc(#{group => <<"Comparison">>}).
282
-doc "`A IS NOT DISTINCT FROM B` — NULL-safe equality.".
283
-spec is_not_distinct_from(expr(), expr()) -> qast:ast_node().
284
is_not_distinct_from(A, B) ->
285
    qast:exp([A, qast:raw(" is not distinct from "), B], #{type => boolean}).
2✔
286

287
%% @TODO type opts
288
-doc(#{group => <<"Arithmetic">>}).
289
-doc "`A + B`. See [Mathematical Operators](https://www.postgresql.org/docs/current/functions-math.html#FUNCTIONS-MATH-OP-TABLE).".
290
-spec '+'(expr(), expr()) -> qast:ast_node().
291
'+'(A, B) ->
292
    qast:exp([qast:raw("("), A, qast:raw(" + "), B, qast:raw(")")]).
4✔
293

294
-doc(#{group => <<"Arithmetic">>}).
295
-doc "`A - B`.".
296
-spec '-'(expr(), expr()) -> qast:ast_node().
297
'-'(A, B) ->
298
    qast:exp([qast:raw("("), A, qast:raw(" - "), B, qast:raw(")")]).
2✔
299

300
-doc(#{group => <<"Arithmetic">>}).
301
-doc "`A * B`.".
302
-spec '*'(expr(), expr()) -> qast:ast_node().
303
'*'(A, B) ->
304
    qast:exp([qast:raw("("), A, qast:raw(" * "), B, qast:raw(")")]).
4✔
305

306
-doc(#{group => <<"Arithmetic">>}).
307
-doc "`A / B` — division. For integers, truncates toward zero.".
308
-spec '/'(expr(), expr()) -> qast:ast_node().
309
'/'(A, B) ->
310
    qast:exp([qast:raw("("), A, qast:raw(" / "), B, qast:raw(")")]).
2✔
311

312
-doc(#{group => <<"Arithmetic">>}).
313
-doc "`abs(A)` — absolute value. See [Math Functions](https://www.postgresql.org/docs/current/functions-math.html#FUNCTIONS-MATH-FUNC-TABLE).".
314
-spec 'abs'(expr()) -> qast:ast_node().
315
abs(A) ->
316
    qast:exp([qast:raw("abs("), A, qast:raw(")")], qast:opts(A)).
2✔
317

318
-doc(#{group => <<"Arithmetic">>}).
319
-doc "`div(A, B)` — integer quotient, truncated toward zero.".
320
-spec 'div'(expr(), expr()) -> qast:ast_node().
321
'div'(A, B) ->
322
    call("div", [A, B], qast:opts(A)).
2✔
323

324
-doc(#{group => <<"Arithmetic">>}).
325
-doc "Alias for [`mod/2`](`mod/2`). Erlang's `rem` operator inside DSL closures rewrites to this.".
326
-spec 'rem'(expr(), expr()) -> qast:ast_node().
327
'rem'(A, B) -> mod(A, B).
2✔
328

329
%% = Numeric functions =========================================================
330

331
-doc(#{group => <<"Numeric">>}).
332
-doc "`mod(A, B)` — remainder of `A / B`. See [Math Functions](https://www.postgresql.org/docs/current/functions-math.html#FUNCTIONS-MATH-FUNC-TABLE).".
333
-spec mod(expr(), expr()) -> qast:ast_node().
334
mod(A, B) ->
335
    call("mod", [A, B], qast:opts(A)).
4✔
336

337
-doc(#{group => <<"Numeric">>}).
338
-doc "`round(A)` — round to nearest integer (banker's rounding for numeric).".
339
-spec round(expr()) -> qast:ast_node().
340
round(A) ->
341
    call("round", [A], qast:opts(A)).
2✔
342

343
-doc(#{group => <<"Numeric">>}).
344
-doc "`round(A, N)` — round to `N` decimal places.".
345
-spec round(expr(), expr()) -> qast:ast_node().
346
round(A, N) ->
347
    call("round", [A, N], qast:opts(A)).
2✔
348

349
-doc(#{group => <<"Numeric">>}).
350
-doc "`ceil(A)` — ceiling (smallest integer not less than `A`).".
351
-spec ceil(expr()) -> qast:ast_node().
352
ceil(A) ->
353
    call("ceil", [A], qast:opts(A)).
2✔
354

355
-doc(#{group => <<"Numeric">>}).
356
-doc "`floor(A)` — floor (largest integer not greater than `A`).".
357
-spec floor(expr()) -> qast:ast_node().
358
floor(A) ->
359
    call("floor", [A], qast:opts(A)).
2✔
360

361
-doc(#{group => <<"Numeric">>}).
362
-doc "`power(A, B)` — `A` raised to the power of `B`.".
363
-spec power(expr(), expr()) -> qast:ast_node().
364
power(A, B) ->
365
    call("power", [A, B], qast:opts(A)).
2✔
366

367
-doc(#{group => <<"Numeric">>}).
368
-doc "`sqrt(A)` — square root.".
369
-spec sqrt(expr()) -> qast:ast_node().
370
sqrt(A) ->
371
    call("sqrt", [A], qast:opts(A)).
2✔
372

373
-doc(#{group => <<"Numeric">>}).
374
-doc "`ln(A)` — natural logarithm.".
375
-spec ln(expr()) -> qast:ast_node().
376
ln(A) ->
377
    call("ln", [A], qast:opts(A)).
2✔
378

379
-doc(#{group => <<"Numeric">>}).
380
-doc "`log(A)` — base-10 logarithm.".
381
-spec log(expr()) -> qast:ast_node().
382
log(A) ->
383
    call("log", [A], qast:opts(A)).
2✔
384

385
-doc(#{group => <<"Numeric">>}).
386
-doc "`log(B, A)` — logarithm of `A` to base `B`.".
387
-spec log(expr(), expr()) -> qast:ast_node().
388
log(B, A) ->
389
    call("log", [B, A], qast:opts(A)).
2✔
390

391
-doc(#{group => <<"Numeric">>}).
392
-doc "`exp(A)` — exponential (`e^A`).".
393
-spec exp(expr()) -> qast:ast_node().
394
exp(A) ->
395
    call("exp", [A], qast:opts(A)).
2✔
396

397
-doc(#{group => <<"Numeric">>}).
398
-doc "`sign(A)` — `-1`, `0`, or `1` according to sign.".
399
-spec sign(expr()) -> qast:ast_node().
400
sign(A) ->
401
    call("sign", [A], qast:opts(A)).
2✔
402

403
-doc(#{group => <<"Numeric">>}).
404
-doc "`random()` — pseudo-random `double precision` in `[0.0, 1.0)`.".
405
-spec random() -> qast:ast_node().
406
random() ->
407
    call("random", [], #{}).
2✔
408

409
%% = LIKE ======================================================================
410

411
-doc(#{group => <<"Pattern matching">>}).
412
-doc "POSIX regex match `A ~ B`. See [POSIX Regular Expressions](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP).".
413
-spec '~'(expr(), expr()) -> qast:ast_node().
414
'~'(A, B) ->
415
    qast:exp([qast:raw("("), A, qast:raw(" ~ "), B, qast:raw(")")], #{type => boolean}).
2✔
416

417
-doc(#{group => <<"Pattern matching">>}).
418
-doc "POSIX regex match (case-insensitive) `A ~* B`.".
419
-spec '~*'(expr(), expr()) -> qast:ast_node().
420
'~*'(A, B) ->
421
    qast:exp([qast:raw("("), A, qast:raw(" ~* "), B, qast:raw(")")], #{type => boolean}).
2✔
422

423
-doc(#{group => <<"Pattern matching">>}).
424
-doc "`A LIKE B`. See [LIKE](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE).".
425
like(A, B) ->
426
    qast:exp([A, qast:raw(" like "), B], #{type => boolean}).
2✔
427

428
-doc(#{group => <<"Pattern matching">>}).
429
-doc "`A ILIKE B` — case-insensitive `LIKE` (PostgreSQL extension).".
430
ilike(A, B) ->
431
    qast:exp([A, qast:raw(" ilike "), B], #{type => boolean}).
2✔
432

433
%% = Aggregators ===============================================================
434

435
-doc(#{group => <<"Aggregates">>}).
436
-doc "`sum(A)`. See [Aggregate Functions](https://www.postgresql.org/docs/current/functions-aggregate.html).".
437
-spec sum(qast:ast_node()) -> qast:ast_node().
438
sum(Ast) ->
439
    call("sum", [Ast], qast:opts(Ast)).
2✔
440

441
-doc(#{group => <<"Aggregates">>}).
442
-doc "`count(A)` — non-NULL row count. Returns `bigint`.".
443
-spec count(qast:ast_node()) -> qast:ast_node().
444
count(Ast) ->
445
    call("count", [Ast], #{type => integer}).
22✔
446

447
-doc(#{group => <<"Aggregates">>}).
448
-doc "`min(A)` — aggregate minimum.".
449
-spec 'min'(expr()) -> qast:ast_node().
450
min(Ast) ->
451
    call("min", [Ast], qast:opts(Ast)).
2✔
452

453
-doc(#{group => <<"Aggregates">>}).
454
-doc "`max(A)` — aggregate maximum.".
455
-spec 'max'(expr()) -> qast:ast_node().
456
max(Ast) ->
457
    call("max", [Ast], qast:opts(Ast)).
4✔
458

459
-doc(#{group => <<"Aggregates">>}).
460
-doc "`DISTINCT A` qualifier — typically used inside another aggregate, e.g. `count(distinct(x))`.".
461
-spec 'distinct'(expr()) -> qast:ast_node().
462
distinct(Ast) ->
463
    call("distinct ", [Ast], qast:opts(Ast)).
2✔
464

465
-doc(#{group => <<"Aggregates">>}).
466
-doc "`array_agg(A)` — collect values into an array. NULLs are included.".
467
-spec 'array_agg'(expr()) -> qast:ast_node().
468
array_agg(Ast) ->
469
    Opts = qast:opts(Ast),
2✔
470
    Type = maps:get(type, Opts, undefined),
2✔
471
    NewOpts = Opts#{type => {array, Type}},
2✔
472
    call("array_agg", [Ast], NewOpts).
2✔
473

474
-type agg_order_spec() :: expr()
475
                        | {expr(), asc | desc}
476
                        | {expr(), asc | desc, nulls_first | nulls_last}.
477
-type agg_order_specs() :: [agg_order_spec(), ...].
478

479
-doc(#{group => <<"Aggregates">>}).
480
-doc "`array_agg(A ORDER BY ...)` — collect into array with explicit ordering.".
481
-spec 'array_agg'(expr(), agg_order_specs()) -> qast:ast_node().
482
array_agg(Ast, OrderSpecs) ->
483
    Opts = qast:opts(Ast),
2✔
484
    Type = maps:get(type, Opts, undefined),
2✔
485
    NewOpts = Opts#{type => {array, Type}},
2✔
486
    qast:exp([
2✔
487
        qast:raw("array_agg("), Ast,
488
        agg_order_by(OrderSpecs),
489
        qast:raw(")")
490
    ], NewOpts).
491

492
-doc(#{group => <<"Aggregates">>}).
493
-doc "`avg(A)` — arithmetic mean. Returns `numeric` or `double precision`.".
494
-spec avg(expr()) -> qast:ast_node().
495
avg(A) ->
496
    call("avg", [A], qast:opts(A)).
2✔
497

498
-doc(#{group => <<"Aggregates">>}).
499
-doc "`bool_and(A)` — true iff every non-NULL input is true.".
500
-spec bool_and(expr()) -> qast:ast_node().
501
bool_and(A) ->
502
    call("bool_and", [A], #{type => boolean}).
2✔
503

504
-doc(#{group => <<"Aggregates">>}).
505
-doc "`bool_or(A)` — true iff any non-NULL input is true.".
506
-spec bool_or(expr()) -> qast:ast_node().
507
bool_or(A) ->
508
    call("bool_or", [A], #{type => boolean}).
2✔
509

510
-doc(#{group => <<"Aggregates">>}).
511
-doc "`every(A)` — SQL standard alias for [`bool_and/1`](`bool_and/1`).".
512
-spec every(expr()) -> qast:ast_node().
513
every(A) ->
514
    call("every", [A], #{type => boolean}).
2✔
515

516
-doc(#{group => <<"Aggregates">>}).
517
-doc "`string_agg(expr, sep)` — concatenate string values with a separator. NULL inputs are ignored.".
518
-spec string_agg(expr(), expr()) -> qast:ast_node().
519
string_agg(Expr, Sep) ->
520
    call("string_agg", [Expr, Sep], #{type => text}).
2✔
521

522
-doc(#{group => <<"Aggregates">>}).
523
-doc """
524
`string_agg(expr, sep ORDER BY ...)` — concatenate with explicit ordering.
525

526
Without `ORDER BY`, `string_agg` is non-deterministic. Use the 3-arg form
527
when result order matters.
528
""".
529
-spec string_agg(expr(), expr(), agg_order_specs()) -> qast:ast_node().
530
string_agg(Expr, Sep, OrderSpecs) ->
531
    qast:exp([
4✔
532
        qast:raw("string_agg("), Expr, qast:raw(","), Sep,
533
        agg_order_by(OrderSpecs),
534
        qast:raw(")")
535
    ], #{type => text}).
536

537
-doc(#{group => <<"Aggregates">>}).
538
-doc "`json_agg(A)` — aggregate into a JSON array. See [JSON Functions](https://www.postgresql.org/docs/current/functions-json.html).".
539
-spec json_agg(expr()) -> qast:ast_node().
540
json_agg(A) ->
541
    call("json_agg", [A], #{type => json}).
2✔
542

543
-doc(#{group => <<"Aggregates">>}).
544
-doc "`json_agg(A ORDER BY ...)`.".
545
-spec json_agg(expr(), agg_order_specs()) -> qast:ast_node().
546
json_agg(A, OrderSpecs) ->
547
    qast:exp([
2✔
548
        qast:raw("json_agg("), A,
549
        agg_order_by(OrderSpecs),
550
        qast:raw(")")
551
    ], #{type => json}).
552

553
-doc(#{group => <<"Aggregates">>}).
554
-doc "`jsonb_agg(A)` — aggregate into a JSONB array.".
555
-spec jsonb_agg(expr()) -> qast:ast_node().
556
jsonb_agg(A) ->
557
    call("jsonb_agg", [A], #{type => jsonb}).
2✔
558

559
-doc(#{group => <<"Aggregates">>}).
560
-doc "`jsonb_agg(A ORDER BY ...)`.".
561
-spec jsonb_agg(expr(), agg_order_specs()) -> qast:ast_node().
562
jsonb_agg(A, OrderSpecs) ->
563
    qast:exp([
2✔
564
        qast:raw("jsonb_agg("), A,
565
        agg_order_by(OrderSpecs),
566
        qast:raw(")")
567
    ], #{type => jsonb}).
568

569
-doc(#{group => <<"Aggregates">>}).
570
-doc "`json_object_agg(K, V)` — aggregate into a JSON object.".
571
-spec json_object_agg(expr(), expr()) -> qast:ast_node().
572
json_object_agg(K, V) ->
573
    call("json_object_agg", [K, V], #{type => json}).
2✔
574

575
-doc(#{group => <<"Aggregates">>}).
576
-doc "`jsonb_object_agg(K, V)` — aggregate into a JSONB object.".
577
-spec jsonb_object_agg(expr(), expr()) -> qast:ast_node().
578
jsonb_object_agg(K, V) ->
579
    call("jsonb_object_agg", [K, V], #{type => jsonb}).
2✔
580

581
-doc(#{group => <<"Aggregates">>}).
582
-doc """
583
`percentile_cont(Frac) WITHIN GROUP (ORDER BY x)` — interpolated continuous
584
percentile. `Frac` ∈ [0, 1].
585

586
See [Ordered-Set Aggregate Functions](https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-ORDEREDSET-TABLE).
587
""".
588
-spec percentile_cont(expr(), expr()) -> qast:ast_node().
589
percentile_cont(Frac, OrderExpr) ->
590
    qast:exp([
2✔
591
        qast:raw("percentile_cont("), Frac,
592
        qast:raw(") within group (order by "), OrderExpr,
593
        qast:raw(")")
594
    ], qast:opts(OrderExpr)).
595

596
-doc(#{group => <<"Aggregates">>}).
597
-doc "`percentile_disc(Frac) WITHIN GROUP (ORDER BY x)` — discrete percentile (picks one of the input values).".
598
-spec percentile_disc(expr(), expr()) -> qast:ast_node().
599
percentile_disc(Frac, OrderExpr) ->
600
    qast:exp([
2✔
601
        qast:raw("percentile_disc("), Frac,
602
        qast:raw(") within group (order by "), OrderExpr,
603
        qast:raw(")")
604
    ], qast:opts(OrderExpr)).
605

606
-doc(#{group => <<"Aggregates">>}).
607
-doc "`mode() WITHIN GROUP (ORDER BY x)` — most frequent value (statistical mode).".
608
-spec mode(expr()) -> qast:ast_node().
609
mode(OrderExpr) ->
610
    qast:exp([
2✔
611
        qast:raw("mode() within group (order by "), OrderExpr,
612
        qast:raw(")")
613
    ], qast:opts(OrderExpr)).
614

615
-doc(#{group => <<"Aggregates">>}).
616
-doc """
617
Wrap any aggregate with `FILTER (WHERE Cond)` — limits which rows the
618
aggregate sees, without affecting the rest of the query. Essential for
619
dashboard-style queries with multiple conditional metrics.
620

621
```erlang
622
#{
623
    total => pg_sql:count(Id),
624
    paid  => pg_sql:filter(pg_sql:count(Id), Status =:= <<"paid">>)
625
}.
626
```
627

628
See [Aggregate Expressions](https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-AGGREGATES).
629
""".
630
-spec filter(qast:ast_node(), expr()) -> qast:ast_node().
631
filter(AggAst, Cond) ->
632
    qast:exp([
4✔
633
        AggAst, qast:raw(" filter (where "), Cond, qast:raw(")")
634
    ], qast:opts(AggAst)).
635

636
agg_order_by(Specs) ->
637
    Exps = lists:map(fun agg_order_spec_exp/1, Specs),
10✔
638
    qast:exp([qast:raw(" order by "), qast:join(Exps, qast:raw(","))]).
10✔
639

640
agg_order_spec_exp({_F, D} = T) when D =:= asc; D =:= desc ->
641
    equery_utils:order_item_exp(T);
4✔
642
agg_order_spec_exp({_F, D, N} = T) when
643
        (D =:= asc orelse D =:= desc),
644
        (N =:= nulls_first orelse N =:= nulls_last) ->
645
    equery_utils:order_item_exp(T);
2✔
646
agg_order_spec_exp(F) -> F.
6✔
647

648
-doc(#{group => <<"Numeric">>}).
649
-doc "`trunc(V, N)` — truncate to `N` decimal places.".
650
-spec 'trunc'(expr(), qast:ast_node() | non_neg_integer()) -> qast:ast_node().
651
'trunc'(V, N) ->
652
    call("trunc", [V, N], qast:opts(V)).
4✔
653

654
%% = Math ======================================================================
655

656
-doc(#{group => <<"Conditional">>}).
657
-doc "`LEAST(A, B)` — minimum of two values, ignoring NULLs. See [Conditional Expressions](https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-GREATEST-LEAST).".
658
-spec 'min'(expr(), expr()) -> qast:ast_node().
659
min(A, B) ->
660
    qast:exp([qast:raw("LEAST("), A, qast:raw(","), B, qast:raw(")")], qast:opts(A)).
2✔
661

662
-doc(#{group => <<"Conditional">>}).
663
-doc "`GREATEST(A, B)` — maximum of two values, ignoring NULLs.".
664
-spec 'max'(expr(), expr()) -> qast:ast_node().
665
max(A, B) ->
666
    qast:exp([qast:raw("GREATEST("), A, qast:raw(","), B, qast:raw(")")], qast:opts(A)).
2✔
667

668
-doc(#{group => <<"Conditional">>}).
669
-doc "`GREATEST(A, B, C, ...)` — N-ary form. Returns max of all non-NULL inputs.".
670
-spec greatest([expr(), ...]) -> qast:ast_node().
671
greatest([H | _] = List) ->
672
    call("GREATEST", List, qast:opts(H)).
6✔
673

674
-doc(#{group => <<"Conditional">>}).
675
-doc "`LEAST(A, B, C, ...)` — N-ary form. Returns min of all non-NULL inputs.".
676
-spec least([expr(), ...]) -> qast:ast_node().
677
least([H | _] = List) ->
678
    call("LEAST", List, qast:opts(H)).
2✔
679

680
%% = Additional operations =====================================================
681

682
-doc(#{group => <<"Misc">>}).
683
-doc "`ROW(...)` constructor with anonymous record type. See [Row Constructors](https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-ROW-CONSTRUCTORS).".
684
-spec row(#{atom() => qast:ast_node()}) -> qast:ast_node().
685
row(Fields) when is_map(Fields) ->
686
    row(undefined, Fields).
2✔
687

688
-doc(#{group => <<"Misc">>}).
689
-doc "`ROW(...)` constructor tagged with a model module (for type inference in row-typed projections).".
690
-spec row(Model :: module(), #{atom() => qast:ast_node()}) -> qast:ast_node().
691
row(Model, Fields) when is_map(Fields) ->
692
    FieldsList = ?MAPS_TO_LIST(Fields),
2✔
693
    Type = {record, {model, Model, [{F, qast:opts(Node)} || {F, Node} <- FieldsList]}},
2✔
694
    qast:exp([
2✔
695
        qast:raw("row("),
696
        qast:join([Node || {_F, Node} <- FieldsList], qast:raw(",")),
8✔
697
        qast:raw(")")
698
    ], #{type => Type}).
699

700
-doc(#{group => <<"Conditional">>}).
701
-doc """
702
`COALESCE(a, b, c, ...)` — returns the first non-NULL argument.
703

704
See [Conditional Expressions](https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-COALESCE-NVL-IFNULL).
705
""".
706
coalesce([H|_]=List) ->
707
    qast:exp([
2✔
708
        qast:raw("coalesce("),
709
        qast:join([Node || Node <- List], qast:raw(",")),
6✔
710
        qast:raw(")")
711
    ], maps:with([type], qast:opts(H))).
712

713
-doc(#{group => <<"Conditional">>}).
714
-doc "Searched `CASE WHEN cond THEN val ... END` without `ELSE` — `NULL` if no branch matches.".
715
-spec case_when([{expr(), expr()}, ...]) -> qast:ast_node().
716
case_when(Whens) ->
717
    case_when(Whens, undefined).
4✔
718

719
-doc(#{group => <<"Conditional">>}).
720
-doc """
721
`CASE WHEN cond THEN val ... ELSE default END`.
722

723
```erlang
724
pg_sql:case_when([
725
    {Id > 100, <<"big">>},
726
    {Id > 10,  <<"medium">>}
727
], <<"small">>).
728
```
729

730
See [Conditional Expressions](https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-CASE).
731
""".
732
-spec case_when([{expr(), expr()}, ...], expr() | undefined) -> qast:ast_node().
733
case_when([{_, FirstThen}|_]=Whens, ElseSpec) ->
734
    Opts = maps:with([type], qast:opts(FirstThen)),
8✔
735
    WhenExps = lists:map(fun({W, T}) ->
8✔
736
        qast:exp([qast:raw(" when "), W, qast:raw(" then "), T])
12✔
737
    end, Whens),
738
    ElseExp = case ElseSpec of
8✔
739
        undefined -> qast:raw("");
4✔
740
        E -> qast:exp([qast:raw(" else "), E])
4✔
741
    end,
742
    qast:exp([
8✔
743
        qast:raw("case"),
744
        qast:exp(WhenExps),
745
        ElseExp,
746
        qast:raw(" end")
747
    ], Opts).
748

749
-doc(#{group => <<"Misc">>}).
750
-doc """
751
`A IN (...)` — membership test.
752

753
- With a `query()` argument, emits `A IN (subquery)`. See [Subquery Expressions](https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-IN).
754
- With a list, emits `A = ANY($1)` (parameterized).
755
- With a single-element list, optimized to `A = item`.
756
""".
757
in(A, #query{}=Q) ->
758
    qast:exp([A, qast:raw(" in ("), qsql:select(Q), qast:raw(")")], #{type => boolean});
2✔
759
in(A, [Item]) ->
760
    '=:='(A, Item);
2✔
761
in(A, B) ->
762
    qast:exp([A, qast:raw(" = ANY("), B, qast:raw(")")], #{type => boolean}).
4✔
763

764
-doc(#{group => <<"Misc">>}).
765
-doc "`EXISTS (subquery)`. See [EXISTS](https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-EXISTS).".
766
exists(#query{}=Q) ->
767
    qast:exp([qast:raw("exists ("), qsql:select(Q), qast:raw(")")], #{type => boolean}).
2✔
768

769
-doc(#{group => <<"Misc">>}).
770
-doc """
771
Build a function call AST: `FunName(Args...)`.
772

773
Used internally by all `pg_sql:` function builders. Public for custom
774
PG functions not yet wrapped by the library:
775

776
```erlang
777
pg_sql:call("my_extension_fn", [Arg1, Arg2], #{type => text}).
778
```
779

780
> #### Warning {: .warning}
781
> `FunName` is **inlined raw**. Never pass user-controlled input here;
782
> use a literal string.
783
""".
784
-spec call(iodata(), [expr()], qast:opts()) -> qast:ast_node().
785
call(FunName, Args, Opts) ->
786
    qast:exp([
210✔
787
        qast:raw([FunName, "("]),
788
        qast:join(Args, qast:raw(",")),
789
        qast:raw(")")
790
    ], Opts).
791

792
%% = Array oprterations ========================================================
793

794
-doc(#{group => <<"Arrays">>}).
795
-doc "`A @> B` — contains. See [Array Functions and Operators](https://www.postgresql.org/docs/current/functions-array.html).".
796
'@>'(A, B) ->
797
    qast:exp([A, qast:raw(" @> "), B], #{type => boolean}).
2✔
798

799
-doc(#{group => <<"Arrays">>}).
800
-doc "`A <@ B` — is contained by.".
801
-spec '<@'(expr(), expr()) -> qast:ast_node().
802
'<@'(A, B) ->
803
    qast:exp([A, qast:raw(" <@ "), B], #{type => boolean}).
2✔
804

805
-doc(#{group => <<"Arrays">>}).
806
-doc "`A && B` — arrays overlap (have any common element).".
807
-spec '&&'(expr(), expr()) -> qast:ast_node().
808
'&&'(A, B) ->
809
    qast:exp([A, qast:raw(" && "), B], #{type => boolean}).
2✔
810

811
-doc(#{group => <<"Arrays">>}).
812
-doc "`array_length(arr, 1)` — length of the first dimension.".
813
-spec array_length(expr()) -> qast:ast_node().
814
array_length(Arr) ->
815
    array_length(Arr, qast:value(1, #{type => integer})).
2✔
816

817
-doc(#{group => <<"Arrays">>}).
818
-doc "`array_length(arr, dim)` — length of dimension `dim`.".
819
-spec array_length(expr(), expr()) -> qast:ast_node().
820
array_length(Arr, Dim) ->
821
    call("array_length", [Arr, Dim], #{type => integer}).
4✔
822

823
-doc(#{group => <<"Arrays">>}).
824
-doc "`array_position(arr, elem)` — 1-based index of `elem` in `arr`, or NULL.".
825
-spec array_position(expr(), expr()) -> qast:ast_node().
826
array_position(Arr, Elem) ->
827
    call("array_position", [Arr, Elem], #{type => integer}).
2✔
828

829
-doc(#{group => <<"Arrays">>}).
830
-doc "`array_append(arr, elem)` — append element.".
831
-spec array_append(expr(), expr()) -> qast:ast_node().
832
array_append(Arr, Elem) ->
833
    call("array_append", [Arr, Elem], qast:opts(Arr)).
2✔
834

835
-doc(#{group => <<"Arrays">>}).
836
-doc "`array_prepend(elem, arr)` — prepend element.".
837
-spec array_prepend(expr(), expr()) -> qast:ast_node().
838
array_prepend(Elem, Arr) ->
839
    call("array_prepend", [Elem, Arr], qast:opts(Arr)).
2✔
840

841
-doc(#{group => <<"Arrays">>}).
842
-doc "`array_remove(arr, elem)` — remove all occurrences of `elem`.".
843
-spec array_remove(expr(), expr()) -> qast:ast_node().
844
array_remove(Arr, Elem) ->
845
    call("array_remove", [Arr, Elem], qast:opts(Arr)).
2✔
846

847
-doc(#{group => <<"Arrays">>}).
848
-doc "`array_replace(arr, from, to)` — replace all `from` elements with `to`.".
849
-spec array_replace(expr(), expr(), expr()) -> qast:ast_node().
850
array_replace(Arr, From, To) ->
851
    call("array_replace", [Arr, From, To], qast:opts(Arr)).
2✔
852

853
-doc(#{group => <<"Arrays">>}).
854
-doc "`array_cat(a, b)` — concatenate two arrays. Equivalent to `a || b`.".
855
-spec array_cat(expr(), expr()) -> qast:ast_node().
856
array_cat(A, B) ->
857
    call("array_cat", [A, B], qast:opts(A)).
2✔
858

859
-doc(#{group => <<"Arrays">>}).
860
-doc """
861
`unnest(arr)` — set-returning function that produces one row per array
862
element.
863

864
Integrates with `q:from/1` and `q:lateral_join/2,3,4`: the resulting
865
table has a single column named `unnest` (PostgreSQL default).
866

867
```erlang
868
q:pipe(q:from(?USER), [
869
    q:lateral_join(inner, fun([#{tags := T}]) ->
870
        q:from(pg_sql:unnest(T))
871
    end),
872
    q:select(fun([#{name := N}, #{unnest := Tag}]) ->
873
        #{name => N, tag => Tag}
874
    end)
875
]).
876
```
877
""".
878
-spec unnest(expr()) -> qast:ast_node().
879
unnest(Arr) ->
880
    ElemType =
10✔
881
        case maps:find(type, qast:opts(Arr)) of
882
            {ok, {array, T}} -> T;
4✔
883
            _ -> undefined
6✔
884
        end,
885
    qast:exp(
10✔
886
        [qast:raw("unnest("), Arr, qast:raw(")")],
887
        #{type => {model, undefined, [{unnest, #{type => ElemType}}]}}
888
    ).
889

890
-doc(#{group => <<"Arrays">>}).
891
-doc """
892
`ARRAY[v1, v2, ...]` constructor. Element type is inferred from the
893
first element's opts.
894

895
Empty array (`ARRAY[]`) needs an explicit cast in PG; use
896
[`as/2`](`as/2`): `pg_sql:as(pg_sql:array([]), {array, int})`.
897
""".
898
-spec array([expr()]) -> qast:ast_node().
899
array(Items) when is_list(Items) ->
900
    Opts = case Items of
8✔
901
        [] -> #{};
2✔
902
        [H | _] ->
903
            ElemType = maps:get(type, qast:opts(H), undefined),
6✔
904
            #{type => {array, ElemType}}
6✔
905
    end,
906
    qast:exp([
8✔
907
        qast:raw("ARRAY["),
908
        qast:join(Items, qast:raw(",")),
909
        qast:raw("]")
910
    ], Opts).
911

912
%% = String / array concat =====================================================
913

914
-doc(#{group => <<"Strings">>}).
915
-doc """
916
`A || B` — concatenation operator.
917

918
NULL-propagating: `'foo' || NULL` → `NULL`. For NULL-skipping behavior
919
use [`concat/2`](`concat/2`). Same operator works for arrays.
920

921
See [String Functions](https://www.postgresql.org/docs/current/functions-string.html).
922
""".
923
-spec '||'(expr(), expr()) -> qast:ast_node().
924
'||'(A, B) ->
925
    qast:exp([qast:raw("("), A, qast:raw(" || "), B, qast:raw(")")], qast:opts(A)).
4✔
926

927
%% = Date/time functions =======================================================
928

929
-doc(#{group => <<"Date/time">>}).
930
-doc "`now()` — current transaction timestamp (with timezone). PostgreSQL extension; equivalent to [`current_timestamp/0`](`current_timestamp/0`).".
931
-spec now() -> qast:ast_node().
932
now() ->
933
    call("now", [], #{type => timestamptz}).
2✔
934

935
-doc(#{group => <<"Date/time">>}).
936
-doc "`current_timestamp` — start-of-transaction timestamp with timezone. Standard SQL.".
937
-spec current_timestamp() -> qast:ast_node().
938
current_timestamp() ->
939
    qast:raw("current_timestamp", #{type => timestamptz}).
2✔
940

941
-doc(#{group => <<"Date/time">>}).
942
-doc "`current_date` — current date (date only).".
943
-spec current_date() -> qast:ast_node().
944
current_date() ->
945
    qast:raw("current_date", #{type => date}).
2✔
946

947
-doc(#{group => <<"Date/time">>}).
948
-doc "`current_time` — current time of day with timezone.".
949
-spec current_time() -> qast:ast_node().
950
current_time() ->
951
    qast:raw("current_time", #{type => timetz}).
2✔
952

953
-type datetime_field() ::
954
        century | day | decade | dow | doy | epoch | hour |
955
        isodow | isoyear | julian | microseconds | millennium |
956
        milliseconds | minute | month | quarter | second |
957
        timezone | timezone_hour | timezone_minute | week | year.
958

959
-spec datetime_field_str(datetime_field()) -> binary().
960
datetime_field_str(century) -> <<"century">>;
×
961
datetime_field_str(day) -> <<"day">>;
4✔
962
datetime_field_str(decade) -> <<"decade">>;
×
963
datetime_field_str(dow) -> <<"dow">>;
2✔
964
datetime_field_str(doy) -> <<"doy">>;
×
965
datetime_field_str(epoch) -> <<"epoch">>;
×
966
datetime_field_str(hour) -> <<"hour">>;
2✔
967
datetime_field_str(isodow) -> <<"isodow">>;
×
968
datetime_field_str(isoyear) -> <<"isoyear">>;
×
969
datetime_field_str(julian) -> <<"julian">>;
×
970
datetime_field_str(microseconds) -> <<"microseconds">>;
×
971
datetime_field_str(millennium) -> <<"millennium">>;
×
972
datetime_field_str(milliseconds) -> <<"milliseconds">>;
×
973
datetime_field_str(minute) -> <<"minute">>;
×
974
datetime_field_str(month) -> <<"month">>;
2✔
975
datetime_field_str(quarter) -> <<"quarter">>;
×
976
datetime_field_str(second) -> <<"second">>;
×
977
datetime_field_str(timezone) -> <<"timezone">>;
×
978
datetime_field_str(timezone_hour) -> <<"timezone_hour">>;
×
979
datetime_field_str(timezone_minute) -> <<"timezone_minute">>;
×
980
datetime_field_str(week) -> <<"week">>;
×
981
datetime_field_str(year) -> <<"year">>.
4✔
982

983
-doc(#{group => <<"Date/time">>}).
984
-doc """
985
`date_trunc('field', source)` — truncate a timestamp to the specified
986
precision. Field is a closed enum (validated at build time).
987

988
See [Date/Time Functions](https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC).
989
""".
990
-spec date_trunc(datetime_field(), expr()) -> qast:ast_node().
991
date_trunc(Field, Source) ->
992
    FieldBin = datetime_field_str(Field),
8✔
993
    call("date_trunc", [qast:value(FieldBin, #{type => text}), Source], qast:opts(Source)).
6✔
994

995
-doc(#{group => <<"Date/time">>}).
996
-doc """
997
`EXTRACT(field FROM source)` — get a sub-field as `numeric`.
998

999
See [EXTRACT](https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT).
1000
""".
1001
-spec extract(datetime_field(), expr()) -> qast:ast_node().
1002
extract(Field, Source) ->
1003
    FieldBin = datetime_field_str(Field),
8✔
1004
    qast:exp([
6✔
1005
        qast:raw("extract("),
1006
        qast:raw(FieldBin),
1007
        qast:raw(" from "),
1008
        Source,
1009
        qast:raw(")")
1010
    ], #{type => numeric}).
1011

1012
-doc(#{group => <<"Date/time">>}).
1013
-doc "`date_part('field', source)` — same as `extract`, returns `double precision`.".
1014
-spec date_part(datetime_field(), expr()) -> qast:ast_node().
1015
date_part(Field, Source) ->
1016
    FieldBin = datetime_field_str(Field),
4✔
1017
    call("date_part", [qast:value(FieldBin, #{type => text}), Source], #{type => float8}).
2✔
1018

1019
-doc(#{group => <<"Date/time">>}).
1020
-doc "`age(t)` — interval since `current_date` to `t` (or vice-versa).".
1021
-spec age(expr()) -> qast:ast_node().
1022
age(A) ->
1023
    call("age", [A], #{type => interval}).
2✔
1024

1025
-doc(#{group => <<"Date/time">>}).
1026
-doc "`age(t1, t2)` — symbolic interval `t1 - t2`.".
1027
-spec age(expr(), expr()) -> qast:ast_node().
1028
age(A, B) ->
1029
    call("age", [A, B], #{type => interval}).
2✔
1030

1031
-doc(#{group => <<"Date/time">>}).
1032
-doc "`to_char(val, fmt)` — format a date/timestamp/number as text. See [Data Type Formatting Functions](https://www.postgresql.org/docs/current/functions-formatting.html).".
1033
-spec to_char(expr(), expr()) -> qast:ast_node().
1034
to_char(A, Fmt) ->
1035
    call("to_char", [A, Fmt], #{type => text}).
2✔
1036

1037
-doc(#{group => <<"Date/time">>}).
1038
-doc "`to_date(text, fmt)` — parse a string into `date`.".
1039
-spec to_date(expr(), expr()) -> qast:ast_node().
1040
to_date(A, Fmt) ->
1041
    call("to_date", [A, Fmt], #{type => date}).
2✔
1042

1043
-doc(#{group => <<"Date/time">>}).
1044
-doc "`to_timestamp(epoch)` — Unix-epoch seconds to `timestamptz`.".
1045
-spec to_timestamp(expr()) -> qast:ast_node().
1046
to_timestamp(A) ->
1047
    call("to_timestamp", [A], #{type => timestamptz}).
2✔
1048

1049
-doc(#{group => <<"Date/time">>}).
1050
-doc "`to_timestamp(text, fmt)` — parse a formatted string into `timestamptz`.".
1051
-spec to_timestamp(expr(), expr()) -> qast:ast_node().
1052
to_timestamp(A, Fmt) ->
1053
    call("to_timestamp", [A, Fmt], #{type => timestamptz}).
2✔
1054

1055
%% = String functions ==========================================================
1056

1057
-doc(#{group => <<"Strings">>}).
1058
-doc "`concat(a, b, c, ...)` — concatenate. **NULLs are skipped** (unlike `||`). See [String Functions](https://www.postgresql.org/docs/current/functions-string.html).".
1059
-spec concat([expr(), ...]) -> qast:ast_node().
1060
concat(List) when is_list(List) ->
1061
    call("concat", List, #{type => text}).
2✔
1062

1063
-doc(#{group => <<"Strings">>}).
1064
-doc "`concat(a, b)` — 2-arg form for convenience.".
1065
-spec concat(expr(), expr()) -> qast:ast_node().
1066
concat(A, B) ->
1067
    call("concat", [A, B], #{type => text}).
2✔
1068

1069
-doc(#{group => <<"Strings">>}).
1070
-doc "`length(s)` — string length in characters.".
1071
-spec length(expr()) -> qast:ast_node().
1072
length(A) ->
1073
    call("length", [A], #{type => integer}).
4✔
1074

1075
-doc(#{group => <<"Strings">>}).
1076
-doc "`char_length(s)` — SQL-standard alias for `length`.".
1077
-spec char_length(expr()) -> qast:ast_node().
1078
char_length(A) ->
1079
    call("char_length", [A], #{type => integer}).
2✔
1080

1081
-doc(#{group => <<"Strings">>}).
1082
-doc "`lower(s)` — lowercase.".
1083
-spec lower(expr()) -> qast:ast_node().
1084
lower(A) ->
1085
    call("lower", [A], qast:opts(A)).
4✔
1086

1087
-doc(#{group => <<"Strings">>}).
1088
-doc "`upper(s)` — uppercase.".
1089
-spec upper(expr()) -> qast:ast_node().
1090
upper(A) ->
1091
    call("upper", [A], qast:opts(A)).
4✔
1092

1093
-doc(#{group => <<"Strings">>}).
1094
-doc "`trim(s)` — strip whitespace from both ends.".
1095
-spec trim(expr()) -> qast:ast_node().
1096
trim(A) ->
1097
    call("trim", [A], qast:opts(A)).
2✔
1098

1099
-doc(#{group => <<"Strings">>}).
1100
-doc "`trim(s, chars)` — strip any character listed in `chars` from both ends.".
1101
-spec trim(expr(), expr()) -> qast:ast_node().
1102
trim(A, Chars) ->
1103
    call("trim", [A, Chars], qast:opts(A)).
2✔
1104

1105
-doc(#{group => <<"Strings">>}).
1106
-doc "`ltrim(s)` — strip whitespace from the left.".
1107
-spec ltrim(expr()) -> qast:ast_node().
1108
ltrim(A) ->
1109
    call("ltrim", [A], qast:opts(A)).
2✔
1110

1111
-doc(#{group => <<"Strings">>}).
1112
-doc "`ltrim(s, chars)` — strip listed characters from the left.".
1113
-spec ltrim(expr(), expr()) -> qast:ast_node().
1114
ltrim(A, Chars) ->
1115
    call("ltrim", [A, Chars], qast:opts(A)).
2✔
1116

1117
-doc(#{group => <<"Strings">>}).
1118
-doc "`rtrim(s)` — strip whitespace from the right.".
1119
-spec rtrim(expr()) -> qast:ast_node().
1120
rtrim(A) ->
1121
    call("rtrim", [A], qast:opts(A)).
2✔
1122

1123
-doc(#{group => <<"Strings">>}).
1124
-doc "`rtrim(s, chars)` — strip listed characters from the right.".
1125
-spec rtrim(expr(), expr()) -> qast:ast_node().
1126
rtrim(A, Chars) ->
1127
    call("rtrim", [A, Chars], qast:opts(A)).
2✔
1128

1129
-doc(#{group => <<"Strings">>}).
1130
-doc "`replace(s, from, to)` — replace all occurrences of `from` with `to`.".
1131
-spec replace(expr(), expr(), expr()) -> qast:ast_node().
1132
replace(A, From, To) ->
1133
    call("replace", [A, From, To], qast:opts(A)).
2✔
1134

1135
-doc(#{group => <<"Strings">>}).
1136
-doc "`split_part(s, sep, n)` — `n`-th field after splitting `s` by `sep` (1-indexed).".
1137
-spec split_part(expr(), expr(), expr()) -> qast:ast_node().
1138
split_part(A, Delim, N) ->
1139
    call("split_part", [A, Delim, N], #{type => text}).
2✔
1140

1141
-doc(#{group => <<"Strings">>}).
1142
-doc "`substring(s, from)` — from the given 1-based start position to end.".
1143
-spec substring(expr(), expr()) -> qast:ast_node().
1144
substring(A, From) ->
1145
    call("substring", [A, From], qast:opts(A)).
2✔
1146

1147
-doc(#{group => <<"Strings">>}).
1148
-doc "`substring(s, from, len)` — `len` characters starting at `from`.".
1149
-spec substring(expr(), expr(), expr()) -> qast:ast_node().
1150
substring(A, From, Len) ->
1151
    call("substring", [A, From, Len], qast:opts(A)).
2✔
1152

1153
-doc(#{group => <<"Strings">>}).
1154
-doc "`strpos(haystack, needle)` — 1-based position of `needle` in `haystack`, or 0.".
1155
-spec strpos(expr(), expr()) -> qast:ast_node().
1156
strpos(Haystack, Needle) ->
1157
    call("strpos", [Haystack, Needle], #{type => integer}).
2✔
1158

1159
-doc(#{group => <<"Strings">>}).
1160
-doc "`starts_with(s, prefix)` — boolean prefix test. Faster than `LIKE 'prefix%'` for indexed lookup.".
1161
-spec starts_with(expr(), expr()) -> qast:ast_node().
1162
starts_with(A, Prefix) ->
1163
    call("starts_with", [A, Prefix], #{type => boolean}).
4✔
1164

1165
-doc(#{group => <<"Strings">>}).
1166
-doc "`regexp_replace(s, pat, repl)` — replace first match. See [POSIX Regex Match](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP).".
1167
-spec regexp_replace(expr(), expr(), expr()) -> qast:ast_node().
1168
regexp_replace(A, Pattern, Repl) ->
1169
    call("regexp_replace", [A, Pattern, Repl], qast:opts(A)).
2✔
1170

1171
-doc(#{group => <<"Strings">>}).
1172
-doc "`regexp_replace(s, pat, repl, flags)` — `'g'` for global replace, `'i'` case-insensitive, etc.".
1173
-spec regexp_replace(expr(), expr(), expr(), expr()) -> qast:ast_node().
1174
regexp_replace(A, Pattern, Repl, Flags) ->
1175
    call("regexp_replace", [A, Pattern, Repl, Flags], qast:opts(A)).
2✔
1176

1177
-doc(#{group => <<"Strings">>}).
1178
-doc "`regexp_match(s, pat)` — returns a `text[]` of capture groups, or NULL if no match.".
1179
-spec regexp_match(expr(), expr()) -> qast:ast_node().
1180
regexp_match(A, Pattern) ->
1181
    call("regexp_match", [A, Pattern], #{type => {array, text}}).
2✔
1182

1183
-doc(#{group => <<"Strings">>}).
1184
-doc "`regexp_match(s, pat, flags)` — with regex flags.".
1185
-spec regexp_match(expr(), expr(), expr()) -> qast:ast_node().
1186
regexp_match(A, Pattern, Flags) ->
1187
    call("regexp_match", [A, Pattern, Flags], #{type => {array, text}}).
2✔
1188

1189
%% = Type functions ============================================================
1190

1191
-doc(#{group => <<"Type casts">>}).
1192
-doc """
1193
`(Ast)::Type` — SQL type cast.
1194

1195
```erlang
1196
pg_sql:as(Id, text)              %% (id)::text
1197
pg_sql:as(V, {varchar, 60})      %% (v)::varchar(60)
1198
pg_sql:as(V, {array, integer})   %% (v)::integer[]
1199
```
1200

1201
See [Type Casts](https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS).
1202

1203
> #### Warning {: .warning}
1204
> `Type` is rendered raw. Use only with literal atoms / tuples; never
1205
> with user-controlled values.
1206
""".
1207
as(Ast, Type) ->
1208
    Opts = qast:opts(Ast),
2✔
1209
    qast:exp([
2✔
1210
        qast:raw("("), Ast, qast:raw(")::"),
1211
        qast:raw(type_str(Type))
1212
    ], Opts#{type => Type}).
1213

1214
-doc(#{group => <<"Type casts">>}).
1215
-doc """
1216
Annotate an AST node with a type, **without** emitting an SQL cast.
1217

1218
Useful when you know the inferred type is wrong (e.g. when wrapping a
1219
[`call/3`](`call/3`) to a function whose return type isn't auto-inferred)
1220
and want correct downstream type propagation without paying for a
1221
runtime cast.
1222
""".
1223
set_type(Ast, Type) ->
1224
    Opts = qast:opts(Ast),
2✔
1225
    qast:set_opts(Ast, Opts#{type => Type}).
2✔
1226

1227
type_str(Atom) when is_atom(Atom) ->
1228
    atom_to_binary(Atom, utf8);
6✔
1229
type_str({array, Atom}) when is_atom(Atom) ->
1230
    iolist_to_binary([type_str(Atom), "[]"]);
2✔
1231
type_str({Type, Args}) when Type =/= array ->
1232
    iolist_to_binary([
4✔
1233
        to_iodata(Type),
1234
        "(", join([to_iodata(A) || A <- Args], ","), ")"
6✔
1235
    ]).
1236

1237
to_iodata(Atom) when is_atom(Atom) ->
1238
    atom_to_list(Atom);
4✔
1239
to_iodata(D) when is_list(D); is_binary(D) ->
1240
    D;
2✔
1241
to_iodata(Int) when is_integer(Int) ->
1242
    integer_to_list(Int);
2✔
1243
to_iodata(Float) when is_float(Float) ->
1244
    io_lib:format("~p", [Float]).
2✔
1245

1246
join([], _) -> [];
2✔
1247
join([H|T],Sep) -> [H|[[Sep,E]||E<-T]].
2✔
1248

1249
-ifdef(TEST).
1250
-include_lib("eunit/include/eunit.hrl").
1251

1252
type_str_test() ->
1253
    ?assertEqual(<<"bigint">>, type_str(bigint)),
2✔
1254
    ?assertEqual(<<"int[]">>, type_str({array, int})),
2✔
1255
    ?assertEqual(<<"custom()">>, type_str({custom, []})),
2✔
1256
    ?assertEqual(<<"custom(a,1,2.0)">>, type_str({custom, [<<"a">>, 1, 2.0]})).
2✔
1257

1258
-endif.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc