Skip to content

Commit 57f5ca3

Browse files
committed
fix: allow to pass expression to query builder column default values
- fixed value functions in query builder - added optional link to bug.yml template - added heads up about flow-php/postgresql being in development
1 parent 8844204 commit 57f5ca3

File tree

9 files changed

+311
-2
lines changed

9 files changed

+311
-2
lines changed

.github/ISSUE_TEMPLATE/bug.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ body:
2424
render: php
2525
validations:
2626
required: true
27+
- type: input
28+
id: playground-url
29+
attributes:
30+
label: Playground snippet
31+
description: "Share a snippet from the Flow PHP Playground to help us reproduce the issue."
32+
placeholder: "https://flow-php.com/playground/?snippet=..."
33+
validations:
34+
required: false
2735
- type: textarea
2836
id: data
2937
attributes:

documentation/components/libs/postgresql.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
## Overview
1111

12+
> **Note:** This library is under active development. If you encounter any issues, especially with the Query Builder,
13+
> please [report a bug](https://github.com/flow-php/flow/issues/new?template=bug.yml).
14+
1215
PostgreSQL library provides three main capabilities:
1316

1417
1. **SQL Parser** - Parse, analyze, and modify existing PostgreSQL queries using the real PostgreSQL

src/lib/postgresql/src/Flow/PostgreSql/DSL/functions.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
Parameter,
7373
RawExpression,
7474
RowExpression,
75+
SQLValueFunctionExpression,
7576
Star,
7677
Subquery,
7778
TypeCast,
@@ -817,6 +818,51 @@ function data_type_interval() : DataType
817818
return DataType::interval();
818819
}
819820

821+
/**
822+
* SQL standard CURRENT_TIMESTAMP function.
823+
*
824+
* Returns the current date and time (at the start of the transaction).
825+
* Useful as a column default value or in SELECT queries.
826+
*
827+
* Example: column('created_at', data_type_timestamp())->default(current_timestamp())
828+
* Example: select()->select(current_timestamp()->as('now'))
829+
*/
830+
#[DocumentationDSL(module: Module::PG_QUERY, type: DSLType::HELPER)]
831+
function current_timestamp() : SQLValueFunctionExpression
832+
{
833+
return SQLValueFunctionExpression::currentTimestamp();
834+
}
835+
836+
/**
837+
* SQL standard CURRENT_DATE function.
838+
*
839+
* Returns the current date (at the start of the transaction).
840+
* Useful as a column default value or in SELECT queries.
841+
*
842+
* Example: column('birth_date', data_type_date())->default(current_date())
843+
* Example: select()->select(current_date()->as('today'))
844+
*/
845+
#[DocumentationDSL(module: Module::PG_QUERY, type: DSLType::HELPER)]
846+
function current_date() : SQLValueFunctionExpression
847+
{
848+
return SQLValueFunctionExpression::currentDate();
849+
}
850+
851+
/**
852+
* SQL standard CURRENT_TIME function.
853+
*
854+
* Returns the current time (at the start of the transaction).
855+
* Useful as a column default value or in SELECT queries.
856+
*
857+
* Example: column('start_time', data_type_time())->default(current_time())
858+
* Example: select()->select(current_time()->as('now_time'))
859+
*/
860+
#[DocumentationDSL(module: Module::PG_QUERY, type: DSLType::HELPER)]
861+
function current_time() : SQLValueFunctionExpression
862+
{
863+
return SQLValueFunctionExpression::currentTime();
864+
}
865+
820866
/**
821867
* Create a UUID data type.
822868
*/

src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Expression/ExpressionFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ public static function fromAst(Node $node) : Expression
117117
return ArrayExpression::fromAst($node);
118118
}
119119

120+
if ($node->getSqlValueFunction() !== null) {
121+
return SQLValueFunctionExpression::fromAst($node);
122+
}
123+
120124
throw UnsupportedNodeException::forNodeType('Unknown expression node type');
121125
}
122126
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\PostgreSql\QueryBuilder\Expression;
6+
7+
use Flow\PostgreSql\Protobuf\AST\{Node, SQLValueFunction, SQLValueFunctionOp};
8+
use Flow\PostgreSql\QueryBuilder\Exception\{InvalidAstException, UnsupportedNodeException};
9+
10+
/**
11+
* Represents SQL standard value functions like CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME.
12+
*
13+
* These are special SQL keywords that return values based on the current transaction time.
14+
* Unlike regular functions, they don't use parentheses and are represented in PostgreSQL's
15+
* AST as SQLValueFunction nodes rather than FuncCall nodes.
16+
*/
17+
final readonly class SQLValueFunctionExpression implements Expression
18+
{
19+
private function __construct(
20+
private int $op,
21+
) {
22+
}
23+
24+
public static function currentCatalog() : self
25+
{
26+
return new self(SQLValueFunctionOp::SVFOP_CURRENT_CATALOG);
27+
}
28+
29+
public static function currentDate() : self
30+
{
31+
return new self(SQLValueFunctionOp::SVFOP_CURRENT_DATE);
32+
}
33+
34+
public static function currentRole() : self
35+
{
36+
return new self(SQLValueFunctionOp::SVFOP_CURRENT_ROLE);
37+
}
38+
39+
public static function currentSchema() : self
40+
{
41+
return new self(SQLValueFunctionOp::SVFOP_CURRENT_SCHEMA);
42+
}
43+
44+
public static function currentTime() : self
45+
{
46+
return new self(SQLValueFunctionOp::SVFOP_CURRENT_TIME);
47+
}
48+
49+
public static function currentTimestamp() : self
50+
{
51+
return new self(SQLValueFunctionOp::SVFOP_CURRENT_TIMESTAMP);
52+
}
53+
54+
public static function currentUser() : self
55+
{
56+
return new self(SQLValueFunctionOp::SVFOP_CURRENT_USER);
57+
}
58+
59+
public static function fromAst(Node $node) : static
60+
{
61+
$sqlValueFunction = $node->getSqlValueFunction();
62+
63+
if ($sqlValueFunction === null) {
64+
throw InvalidAstException::unexpectedNodeType('SQLValueFunction', 'unknown');
65+
}
66+
67+
$op = $sqlValueFunction->getOp();
68+
69+
$supported = [
70+
SQLValueFunctionOp::SVFOP_CURRENT_DATE,
71+
SQLValueFunctionOp::SVFOP_CURRENT_TIME,
72+
SQLValueFunctionOp::SVFOP_CURRENT_TIMESTAMP,
73+
SQLValueFunctionOp::SVFOP_CURRENT_ROLE,
74+
SQLValueFunctionOp::SVFOP_CURRENT_USER,
75+
SQLValueFunctionOp::SVFOP_CURRENT_CATALOG,
76+
SQLValueFunctionOp::SVFOP_CURRENT_SCHEMA,
77+
SQLValueFunctionOp::SVFOP_LOCALTIME,
78+
SQLValueFunctionOp::SVFOP_LOCALTIMESTAMP,
79+
SQLValueFunctionOp::SVFOP_SESSION_USER,
80+
SQLValueFunctionOp::SVFOP_USER,
81+
];
82+
83+
if (!\in_array($op, $supported, true)) {
84+
throw UnsupportedNodeException::forNodeType('SQLValueFunction op=' . $op);
85+
}
86+
87+
return new self($op);
88+
}
89+
90+
public static function localTime() : self
91+
{
92+
return new self(SQLValueFunctionOp::SVFOP_LOCALTIME);
93+
}
94+
95+
public static function localTimestamp() : self
96+
{
97+
return new self(SQLValueFunctionOp::SVFOP_LOCALTIMESTAMP);
98+
}
99+
100+
public static function sessionUser() : self
101+
{
102+
return new self(SQLValueFunctionOp::SVFOP_SESSION_USER);
103+
}
104+
105+
public static function user() : self
106+
{
107+
return new self(SQLValueFunctionOp::SVFOP_USER);
108+
}
109+
110+
public function as(string $alias) : AliasedExpression
111+
{
112+
return AliasedExpression::create($this, $alias);
113+
}
114+
115+
public function toAst() : Node
116+
{
117+
$sqlValueFunction = new SQLValueFunction();
118+
$sqlValueFunction->setOp($this->op);
119+
$sqlValueFunction->setTypmod(-1);
120+
121+
$node = new Node();
122+
$node->setSqlValueFunction($sqlValueFunction);
123+
124+
return $node;
125+
}
126+
}

src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Schema/ColumnDefinition.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Flow\PostgreSql\Protobuf\AST\{ColumnDef, ConstrType, Constraint, Node, RangeVar};
99
use Flow\PostgreSql\Protobuf\AST\PBString;
1010
use Flow\PostgreSql\QueryBuilder\Exception\InvalidAstException;
11+
use Flow\PostgreSql\QueryBuilder\Expression\Expression;
1112

1213
final readonly class ColumnDefinition
1314
{
@@ -47,13 +48,17 @@ public function check(string $expression) : self
4748
);
4849
}
4950

50-
public function default(bool|float|int|string|null $value) : self
51+
public function default(bool|float|int|string|Expression|null $value) : self
5152
{
53+
$node = $value instanceof Expression
54+
? $value->toAst()
55+
: $this->createLiteralNode($value);
56+
5257
return new self(
5358
$this->name,
5459
$this->type,
5560
$this->notNull,
56-
$this->createLiteralNode($value),
61+
$node,
5762
$this->identity,
5863
$this->generatedExpression,
5964
$this->constraints,

src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/SelectBuilderTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
cond_not,
2525
cond_or,
2626
cte,
27+
current_date,
28+
current_time,
29+
current_timestamp,
2730
data_type_integer,
2831
data_type_text,
2932
derived,
@@ -1225,4 +1228,53 @@ public function test_simple_select_with_where() : void
12251228
'SELECT * FROM users WHERE active = true'
12261229
);
12271230
}
1231+
1232+
public function test_sql_value_function_current_date() : void
1233+
{
1234+
$query = select()
1235+
->select(current_date()->as('today'));
1236+
1237+
$this->assertSelectQueryRoundTrip(
1238+
$query,
1239+
'SELECT current_date AS today'
1240+
);
1241+
}
1242+
1243+
public function test_sql_value_function_current_time() : void
1244+
{
1245+
$query = select()
1246+
->select(current_time()->as('now_time'));
1247+
1248+
$this->assertSelectQueryRoundTrip(
1249+
$query,
1250+
'SELECT current_time AS now_time'
1251+
);
1252+
}
1253+
1254+
public function test_sql_value_function_current_timestamp() : void
1255+
{
1256+
$query = select()
1257+
->select(current_timestamp()->as('now'));
1258+
1259+
$this->assertSelectQueryRoundTrip(
1260+
$query,
1261+
'SELECT current_timestamp AS now'
1262+
);
1263+
}
1264+
1265+
public function test_sql_value_functions_with_other_columns() : void
1266+
{
1267+
$query = select()
1268+
->select(
1269+
col('id'),
1270+
col('name'),
1271+
current_timestamp()->as('created_at')
1272+
)
1273+
->from(table('users'));
1274+
1275+
$this->assertSelectQueryRoundTrip(
1276+
$query,
1277+
'SELECT id, name, current_timestamp AS created_at FROM users'
1278+
);
1279+
}
12281280
}

src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/TableBuilderTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
col,
1111
column,
1212
create,
13+
current_timestamp,
1314
data_type_boolean,
1415
data_type_integer,
1516
data_type_serial,
@@ -328,6 +329,19 @@ public function test_create_table_with_column_default() : void
328329
);
329330
}
330331

332+
public function test_create_table_with_column_default_current_timestamp() : void
333+
{
334+
$builder = create()->table('audit_log')
335+
->column(column('id', data_type_serial())->primaryKey())
336+
->column(column('message', data_type_text())->notNull())
337+
->column(column('created_at', data_type_timestamp())->notNull()->default(current_timestamp()));
338+
339+
$this->assertCreateTableQuery(
340+
$builder,
341+
'CREATE TABLE audit_log (id serial PRIMARY KEY, message pg_catalog.text NOT NULL, created_at timestamp NOT NULL DEFAULT current_timestamp)'
342+
);
343+
}
344+
331345
public function test_create_table_with_composite_primary_key() : void
332346
{
333347
$builder = create()->table('order_items')

0 commit comments

Comments
 (0)