While Condition
and TargetListManipulator
do implement the FragmentBuilder
interface,
their subclasses do not provide means to configure Fragment
s returned by
the getFragment()
method. Only the builders that have special configuration methods are listed below.
Instances of these classes or of their proxy subclasses are created in some FluentBuilder
methods and can be
configured via methods described here.
All the classes described below have subclasses in builders\proxies
namespace. Those classes proxy the methods
of FluentBuilder
instance that returns them allowing to seamlessly chain method calls. They also implement
the Proxy
interface:
namespace sad_spirit\pg_gateway\builders;
use sad_spirit\pg_gateway\Fragment;
use sad_spirit\pg_gateway\FragmentBuilder;
interface Proxy extends FragmentBuilder
{
public function getOwnFragment(): Fragment;
}
getOwnFragment()
method returns the fragment created by the builder itself, while getFragment()
method returns
the fragment built by the proxied instance of FluentBuilder
. This way methods defined in TableGateway
need not care
whether they receive an instance of FluentBuilder
or an instance of Proxy
.
This configures what columns of the table accessed via gateway will be returned in the output list of SELECT
or in the RETURNING
clause of DELETE
/ INSERT
/ UPDATE
.
namespace sad_spirit\pg_gateway\builders;
use sad_spirit\pg_gateway\{
Fragment,
FragmentBuilder
};
use sad_spirit\pg_gateway\fragments\target_list\ColumnAliasStrategy;
class ColumnsBuilder implements FragmentBuilder
{
public function __construct(TableDefinition $definition, bool $returningClause = false);
// defined in FragmentBuilder
public function getFragment() : Fragment;
// methods that configure the list of returned columns
public function none() : $this;
public function star() : $this;
public function all() : $this;
public function only(string[] $onlyColumns) : $this;
public function except(string[] $exceptColumns) : $this;
public function primaryKey() : $this;
// methods that configure aliases for returned columns
public function alias(ColumnAliasStrategy $strategy) : $this;
public function replace(string|string[] $pattern, string|string[] $replacement) : $this;
public function map(array<string, string> $columnMap) : $this;
public function apply(\Closure $closure, ?string $key = null) : $this;
}
none()
removes all columns withself
as relation name from the list (this will be a no-op if applied toRETURNING
),star()
replaces all such columns withself.*
shorthand (this is will be a no-op if applied toSELECT
).
No aliases are possible with these methods.
Four other methods replace all columns having self
as relation name with an explicit list of columns:
all()
lists all the table columns,only()
lists only the given ones,except()
- all columns except the given ones,primaryKey()
- columns that belong to the table's primary key.
With the latter methods, it is possible to assign aliases to columns:
alias()
uses a custom implementation ofColumnAliasStrategy
.replace()
will essentially runpreg_replace
on column names using the given arguments (seefragments\target_list\alias_strategies\PregReplaceStrategy
).map()
will try to find aliases in the explicitly provided map['column name' => 'alias']
(seefragments\target_list\alias_strategies\MapStrategy
).apply()
will call the given$closure
with a column name and use the result as an alias (seefragments\target_list\alias_strategies\ClosureStrategy
). Giving a non-null$key
that somehow identifies the given$closure
will allow caching a query that uses this strategy.
If a strategy returns null
or unmodified column name for a given column, then that column will be left without alias.
As there is currently no ConditionBuilder
interface, this is essentially a builder for WhereClauseFragment
which happens to also implement a getCondition()
method. Getting an unwrapped Condition
may be useful if e.g.
you need to combine it via AND
/ OR
with other conditions.
namespace sad_spirit\pg_gateway\builders;
use sad_spirit\pg_gateway\{
Condition,
Fragment,
SelectBuilder,
TableDefinition
};
class ExistsBuilder extends AdditionalSelectBuilder
{
// defined in FragmentBuilder
public function getFragment() : Fragment;
// inherited from AdditionalSelectBuilder
public function __construct(TableDefinition $base, SelectBuilder $additional);
public function alias(string $alias) : $this;
// returns the Condition
public function getCondition() : Condition;
// methods that configure join condition with the base table
public function joinOn(Condition $condition) : $this;
public function joinOnForeignKey(string[] $keyColumns = []) : $this;
public function joinOnRecursiveForeignKey(bool $fromChild = true, array $keyColumns = []) : $this;
// other configuration methods
public function not() : $this;
}
Methods that configure joins are mostly similar in all the classes that extend AdditionalSelectBuilder
.
joinOn()
uses custom join Condition
. As usual, self
alias in that should reference the $base
table and
joined
alias should reference the $additional
table being joined (i.e. the one inside EXISTS(...)
).
joinOnForeignKey()
is used to join two different tables using a FOREIGN KEY
constraint between them.
If there are multiple FOREIGN KEY
constraints between tables, $keyColumns
can be given to select the one
containing these columns on child side. For example, given the following schema
create table example.employees (
id integer not null generated by default as identity,
name text not null,
constraint employees_pkey primary key (id)
);
create table example.documents (
id integer not null generated by default as identity,
author_id integer not null,
approver_id integer,
contents text not null,
constraint documents_pkey primary key (id),
constraint documents_author_fkey foreign key (author_id)
references example.employees (id),
constraint documents_approval_fkey foreign key (approver_id)
references example.employees (id)
);
the following code
use sad_spirit\pg_gateway\builders\FluentBuilder;
$gwEmployees = $locator->createGateway('example.employees');
// selects all employees who authored documents
$selectAuthor = $gwEmployees->select(fn (FluentBuilder $builder) => $builder
->exists($locator->createGateway('example.documents'))
->joinOnForeignKey(['author_id']));
echo $selectAuthor->createSelectStatement()->getSql() . ";\n\n";
// selects all employees who approved documents
$selectApprover = $gwEmployees->select(fn (FluentBuilder $builder) => $builder
->exists($locator->createGateway('example.documents'))
->joinOnForeignKey(['approver_id']));
echo $selectApprover->createSelectStatement()->getSql() . ";\n\n";
will output something similar to
select self.*
from example.employees as self
where exists(
select 1
from example.documents as gw_1
where gw_1.author_id = self.id
);
select self.*
from example.employees as self
where exists(
select 1
from example.documents as gw_2
where gw_2.approver_id = self.id
);
joinOnRecursiveForeignKey()
performs a self-join using a recursive foreign key (i.e. this should be used if
$base
and $additional
reference the same table). $fromChild
specifies whether base table is on the child side
of join or the parent one. For example, given the following table
create table example.tree (
id integer not null generated by default as identity,
parent_id integer,
name text not null,
constraint tree_pkey primary key (id),
constraint tree_parent_fkey foreign key (parent_id)
references example.tree (id)
);
the following code
use sad_spirit\pg_gateway\builders\FluentBuilder;
$gwTree = $locator->createGateway('example.tree');
// selects all items having a parent (this is of course achieved easier with `parent_id IS NOT NULL`)
$selectChild = $gwTree->select(fn (FluentBuilder $builder) => $builder
->exists($gwTree)
->joinOnRecursiveForeignKey(true));
echo $selectChild->createSelectStatement()->getSql() . ";\n\n";
// selects all items having children
$selectParent = $gwTree->select(fn (FluentBuilder $builder) => $builder
->exists($gwTree)
->joinOnRecursiveForeignKey(false));
echo $selectParent->createSelectStatement()->getSql() . ";\n\n";
will output something similar to
select self.*
from example.tree as self
where exists(
select 1
from example.tree as gw_1
where self.parent_id = gw_1.id
);
select self.*
from example.tree as self
where exists(
select 1
from example.tree as gw_2
where gw_2.parent_id = self.id
);
$keyColumns
serve the same purpose as in joinOnForeignKey()
, in the unlikely scenario that there are
multiple recursive FOREIGN KEY
constraints defined.
alias()
specifies an explicit alias for the table inside EXISTS(...)
, a generated one will be used if not given.
not()
toggles generation of NOT EXISTS(...)
condition.
This configures joining a SelectBuilder
object to the current statement.
namespace sad_spirit\pg_gateway\builders;
use sad_spirit\pg_gateway\{
Condition,
Fragment,
SelectBuilder,
TableDefinition,
fragments\JoinStrategy
};
class JoinBuilder extends AdditionalSelectBuilder
{
// defined in FragmentBuilder
public function getFragment() : Fragment;
// inherited from AdditionalSelectBuilder
public function __construct(TableDefinition $base, SelectBuilder $additional);
public function alias(string $alias) : $this;
// methods that configure the type of join being made
public function strategy(JoinStrategy $strategy) : $this;
public function inline() : $this;
public function inner() : $this;
public function left() : $this;
public function right() : $this;
public function full() : $this;
public function lateral() : $this;
public function lateralInner() : $this;
public function lateralLeft() : $this;
// methods that configure join condition with the base table
public function on(Condition $condition) : $this;
public function onForeignKey(string[] $keyColumns = []) : $this;
public function onRecursiveForeignKey(bool $fromChild = true, string[] $keyColumns = []) : $this;
public function unconditional() : $this;
// other configuration methods
public function priority(int $priority) : $this;
public function useForCount(bool $use) : $this;
}
Actual merging of the $additional
to the $base
is performed by an implementation of JoinStrategy
.
strategy()
uses a custom implementation ofJoinStrategy
.inline()
adds the joined table a separate item of the base statement'sFROM
(orUSING
) clause (seefragments\join_strategies\InlineStrategy
). This is the only strategy that works withUPDATE
andDELETE
, using theemployees
/documents
schema above, the following code
$gwDocuments = $locator->createGateway('example.documents');
$delete = $gwDocuments->createDeleteStatement(
$locator->createBuilder('example.documents')
->join(
$locator->createGateway('example.employees')
->selectByPrimaryKey(1)
)
->inline()
->getFragment()
);
echo $delete->getSql();
will output something like
delete from example.documents as self
using example.employees as gw_1
where gw_1.id = $1::int4
inner()
, left()
, right()
, and full()
are backed by fragments\join_strategies\ExplicitJoinStrategy
, they
join the $additional
to the $base
table using the explicit JOIN
clause with the condition as its ON
clause.
$additional
may be wrapped in a subquery if it contains complex clauses.
lateral()
, lateralInner()
, and lateralLeft()
are backed by fragments\join_strategies\LateralSubselectStrategy
,
they wrap the $additional
into the LATERAL
subquery and either put it as a separate FROM
item (lateral()
)
or join to the $base
using INNER
or LEFT
join. The main difference to the previous strategies is that
the condition will be added to the WHERE
clause of subquery rather than to the ON
clause of JOIN
.
inline()
is the default join strategy.
The join condition is configured the same way as in ExistsBuilder
above, unconditional()
method is used
to explicitly state that no join condition is used, as FluentBuilder::join()
will try to join
on a foreign key by default.
priority()
controls the order in which joins will be applied, this is especially useful for LATERAL
joins.
Fragment
s having the higher priority will be applied earlier.
useForCount()
controls whether the join will be performed in SELECT COUNT(*)
query executed by
SelectProxy::executeCount()
. A join that does not modify the number of returned rows can be safely skipped.
This behaves as a FragmentBuilder
returning an instance of SelectListFragment
,
but also has a getManipulator()
method returning an unwrapped instance of TargetListManipulator
which can
be used in ReturningClauseFragment
.
namespace sad_spirit\pg_gateway\builders;
class ScalarSubqueryBuilder extends AdditionalSelectBuilder
{
// defined in FragmentBuilder
public function getFragment() : Fragment;
// inherited from AdditionalSelectBuilder
public function __construct(TableDefinition $base, SelectProxy $additional);
public function alias(string $alias) : $this;
// Returns the unwrapped manipulator
public function getManipulator() : TargetListManipulator;
// methods that configure join condition with the base table
public function joinOn(Condition $condition) : $this;
public function joinOnForeignKey(string[] $keyColumns = []) : $this;
public function joinOnRecursiveForeignKey(bool $fromChild = true, array $keyColumns = []) : $this;
// alias methods
public function tableAlias(string $alias) : $this;
public function columnAlias(string $alias) : $this;
}
The somewhat new methods are
tableAlias()
- this is actually a synonym foralias()
, added to differentiate fromcolumnAlias()
.columnAlias()
- sets the alias for subquery expression in theTargetList
,(SELECT ...) as $alias
.
This is actually used only for with\SelectProxyFragment
subclass of WithClauseFragment
, its proxy subclass
is returned by FluentBuilder::withSelect()
.
namespace sad_spirit\pg_gateway\builders;
use sad_spirit\pg_gateway\{
Fragment,
FragmentBuilder,
SelectProxy
};
class WithClauseBuilder implements FragmentBuilder
{
public function __construct(SelectProxy $select, string $alias);
// defined in FragmentBuilder
public function getFragment() : Fragment;
public function columnAliases(array $aliases) : $this;
public function materialized() : $this;
public function notMaterialized() : $this;
public function recursive() : $this;
public function priority(int $priority) : $this;
}
Note that the package will not generate an alias for a query in WITH
,
so an alias should always be passed to constructor.
recursive()
enables theRECURSIVE
option for theWITH
clause;materialized()
/notMaterialized()
enable[NOT] MATERIALIZED
options for the CTE;columnAliases()
sets the column aliases for the CTE;priority()
sets the Fragment's priority: withoutRECURSIVE
queries inWITH
can only reference their previous siblings, so priority may be important.