priority
Standard
status
Open

qcodo
0.4.9 (Development)
category
General Framework
php
Any



Votes
There is 1 vote for this issue:
comet7

reported: saturday, january 2, 2010, 6:35 am pst  |  by: Fernando Lordán  |  messages: 31 messages  |  last: about 2 years ago


#1  |  Fernando Lordán (Barcelona, CAT, Spain) Spain
Saturday, January 2, 2010, 6:35 AM PST

Currently subqueries can be issued using the “QQ::SubSql( $strSql )” node clause, but that means the need to handle hand-made SQL statements that break the DB-engine agnosticism of the application.

It could be very convenient if the QQueryBuilder could be used to build those subquery SQL statements. This way, a subquery could be composed as any other query, and the resulting SQL statement could be used as a “QQ to SQL” intermediate pass to get subquery nodes.

qcodo_query_methods.tpl

<?php

/**
 * Static Qcodo method to get the SQL statement of a query.
 * Uses BuildQueryStatment to perform most of the work.
 * @param QQCondition $objConditions any conditions on the query, itself
 * @param QQClause[] $objOptionalClausees additional optional QQClause objects for this query
 * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with
 * @return string the SQL statement for the query
 */
public static function GetQuerySqlStatement(QQCondition $objConditions$objOptionalClauses null$mixParameterArray null) {
    
// Get the Query Statement
    
try {
        return <%= 
$objTable->ClassName %>::BuildQueryStatement($objQueryBuilder$objConditions$objOptionalClauses$mixParameterArrayfalse);
    } catch (
QCallerException $objExc) {
        
$objExc->IncrementOffset();
        throw 
$objExc;
    }
}

?>

This method, by the way, could also be used to log or debug SQL queries, as currently there's no way to log queries or take a look at a query when it's not well-formed or throws an exception.

But returning to the subqueries topic, this partial solution leaves a problem: the potential collisions between query and subquery aliases. I already solved this in a non generic manner, but here's a generalization of the idea I used, not difficult to implement thanks to Qcodo's elegant class designs:


SUBQUERIES IMPLEMENTATION

Let's consider the construction of any subquery (and any query, in fact) as identified by an unique SQL-safe id string. When building the (sub)query, we will suffix any aliases that can collide with a special named wildcard. Later, when preparing the (sub)query SQL statement, we'll substitute this wildcard with the (sub)query identifier string, that will be empty if it's not a subquery.

Let's assume the named wildcard is '{_SubqueryId}', although a more unusual name could be used to avoid possible named arguments collisions, and better if this name is used as a PHP defined constant. I'll also use the QQNamedValue::DelimiterCode character to delimite it, although a more specific constant should be defined for this.

When building queries, all table aliases should be suffixed with this named wildcard. A slight modification in the QQueryBuilder->GetTableAlias() method is enough for this purpose, as I've checked it's always used when composing table aliases and also JOIN aliases.

QQuery.class.php

<?php
class QQueryBuilder extends QBaseClass {

[...]

    public function 
GetTableAlias($strTableName) {
        if (!
array_key_exists($strTableName$this->strTableAliasArray)) {
            
$strTableAlias 't' $this->intTableAliasCount++;
            
$this->strTableAliasArray[$strTableName] = $strTableAlias;
        } else {
            
$strTableAlias $this->strTableAliasArray[$strTableName];
        }

        return 
$strTableAlias chr(QQNamedValue::DelimiterCode) . '{_SubqueryId}';
    }

[...]
?>

However, I've observed that there are two places where columns are assigned aliases not based in table aliases. These “fixed” column aliases should also be suffixed with the named wildcard to avoid collisions between query and subqueries column aliases.

QQuery.class.php

<?php
class QQueryBuilder extends QBaseClass {

[...]

    public function 
AddSelectItem($strTableName$strColumnName$strFullAlias) {
        
$strTableAlias $this->GetTableAlias($strTableName);

        if (!
array_key_exists($strFullAlias$this->strColumnAliasArray)) {
            
$strColumnAlias 'a' $this->intColumnAliasCount++;
            
$this->strColumnAliasArray[$strFullAlias] = $strColumnAlias;
        } else {
            
$strColumnAlias $this->strColumnAliasArray[$strFullAlias];
        }

        
$this->strSelectArray[$strFullAlias] = sprintf('%s%s%s.%s%s%s AS %s%s%s%s',
            
$this->strEscapeIdentifierBegin$strTableAlias$this->strEscapeIdentifierEnd,
            
$this->strEscapeIdentifierBegin$strColumnName$this->strEscapeIdentifierEnd,
            
$this->strEscapeIdentifierBegin$strColumnAliaschr(QQNamedValue::DelimiterCode) . '{_SubqueryId}'$this->strEscapeIdentifierEnd);
    }
        
    public function 
AddSelectFunction($strFunctionName$strColumnName$strFullAlias) {
        
$this->strSelectArray[$strFullAlias] = sprintf('%s(%s) AS %s__%s%s%s',
            
$strFunctionName$strColumnName,
            
$this->strEscapeIdentifierBegin$strFullAliaschr(QQNamedValue::DelimiterCode) . '{_SubqueryId}'$this->strEscapeIdentifierEnd);
    }

[...]
?>

This wildcard suffix should be always substituted with the subquery id (empty if not a subquery) in the QDataBase->PrepareStatement() method, the same way as named parameters are.

QDatabaseBase.class.php

<?php
public function PrepareStatement($strQuery$mixParameterArray) {
    foreach (
$mixParameterArray as $strKey => $mixValue) {
        if (
is_array($mixValue)) {
            
$strParameters = array();
            foreach (
$mixValue as $mixParameter)
                
array_push($strParameters$this->Database->SqlVariable($mixParameter));
            
$strQuery str_replace(chr(QQNamedValue::DelimiterCode) . '{' $strKey '}'implode(','$strParameters) . ')'$strQuery);
        } else if (
$strKey == '_SubqueryId') {
            
// Alias suffix substitution.
            
$strQuery str_replace(chr(QQNamedValue::DelimiterCode) . '{' $strKey '}''_' QType::Cast($mixValueQType::String), $strQuery);
        } else {
            
$strQuery str_replace(chr(QQNamedValue::DelimiterCode) . '{=' $strKey '=}'$this->SqlVariable($mixValuetruefalse), $strQuery);
            
$strQuery str_replace(chr(QQNamedValue::DelimiterCode) . '{!' $strKey '!}'$this->SqlVariable($mixValuetruetrue), $strQuery);
            
$strQuery str_replace(chr(QQNamedValue::DelimiterCode) . '{' $strKey '}'$this->SqlVariable($mixValue), $strQuery);
        }
    }
    
// Alias suffix substitution if no suffix specified (not a subquery).
    
$strQuery str_replace(chr(QQNamedValue::DelimiterCode) . '{_SubqueryId}'''$strQuery);

    return 
$strQuery;
}
?>

With this, we've added support for suffixing all the query aliases (table, JOIN and column aliases) with an optional (sub)query identifier, that can be specified as a BuildQuery parameter.

Finally, when getting an SQL statement specifically for a subquery, a query id should be forced to be used as alias suffix.

qcodo_query_methods.tpl

<?php

/**
 * Static Qcodo method to get the SQL statement for a subquery.
 * Uses BuildQueryStatment to perform most of the work.
 * @param QQCondition $objConditions any conditions on the query, itself
 * @param QQClause[] $objOptionalClausees additional optional QQClause objects for this query
 * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with
 * @return string the SQL statement for the subquery
 */
public static function GetSubquerySqlStatement(QQCondition $objConditions$objOptionalClauses null$mixParameterArray null) {

    if (!
is_array($mixParameterArray)) {
        
$mixParameterArray = Array();
    }
    if (!
array_key_exists('_SubqueryId'$mixParameterArray)) {
        
$mixParameterArray['_SubqueryId'] = uniqid('_subq_');
    }

    
// Get the subquery statement
    
try {
        return <%= 
$objTable->ClassName %>::BuildQueryStatement($objQueryBuilder$objConditions$objOptionalClauses$mixParameterArrayfalse);
    } catch (
QCallerException $objExc) {
        
$objExc->IncrementOffset();
        throw 
$objExc;
    }
}

?>

That would work and avoid aliases collisions. In fact, I've used a particularization of this approach in one application with full success.

Take care of never specifying a  '_SubqueryId'  value when composing the SQL of the main query, as it's only for subqueries. If you accidentally do, the InstantiateDbRow() logic could not work for the main query result.

With this simple implementation you still can't use a subquery as a subtable in the FROM clause, but you can do almost anything else. In fact, it allows to use DB-engine agnostic subqueries in condition clauses and also subquery expansions:

- Subquery node for condition clauses:

QQ::SubSql(
    <ClassName>::GetSubquerySqlStatement(
        <conditions>,
        <clauses>
    )
    [, <substitution QQNode 1>]
    [, <substitution QQNode 2>]
    [, etc.]
)

- Subquery expansion of a query:

QQ::Expand(
    QQ::Virtual( $strVirtualName,
        QQ::SubSql( 
            <ClassName>::GetSubquerySqlStatement(
                <conditions>,
                <clauses>
            )
            [, <substitution QQNode 1>]
            [, <substitution QQNode 2>]
            [, etc.]
        )
    )
)

Additionally, this also brings full support for nested subqueries, with any nesting level.

There's one limitation in this subquery nesting, however: when using named arguments in subqueries, substitution QQNodes always inherit the QQueryBuilder from its “parent” query when being converted to column names/aliases, you can't choose to which subquery nesting level they refer to. So for example, in a query like ( Query 1 ( Subquery 2 ( Subquery 3 ) ) ), you can't substitute a named argument in (Subquery 3) with a query node from (Query 1), but only with a query node that refers to (Subquery 2). However, I think that will be enough for most scenarios.

As a final improvement, the “QQ to SQL” intermediate pass to get subquery nodes could be hidden by including a factory method for QQSubQuerySqlNode nodes in each active record class:

qcodo_query_methods.tpl

<?php

/**
 * Static Qcodo method to get a subquery node for a <%= $objTable->ClassName %>-based subquery.
 * Uses BuildQueryStatment to perform most of the work.
 * @param QQCondition $objConditions any conditions on the query, itself
 * @param QQClause[] $objOptionalClausees additional optional QQClause objects for this query
 * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with
 * @return string the SQL statement for the query
 */
public static function Subquery(QQCondition $objConditions$objOptionalClauses null$mixParameterArray null) {
    if (!
is_array($mixParameterArray)) {
        
$mixParameterArray = Array();
    }
    if (!
array_key_exists('_SubqueryId'$mixParameterArray)) {
        
$mixParameterArray['_SubqueryId'] = uniqid('_subq_');
    }

    
// Get the subquery statement
    
try {
        
$strQuery = <%= $objTable->ClassName %>::BuildQueryStatement($objQueryBuilder$objConditions$objOptionalClauses$mixParameterArrayfalse);
    } catch (
QCallerException $objExc) {
        
$objExc->IncrementOffset();
        throw 
$objExc;
    }

    
$objParentQueryNodeArray func_get_args();
    
array_shift($objParentQueryNodeArray);
    
array_shift($objParentQueryNodeArray);
    
array_shift($objParentQueryNodeArray);

    return new 
QQSubQuerySqlNode($strQuery$objParentQueryNodeArray);
}

?>

This way, the usage of subqueries would be simplified in an elegant manner:

- Subquery node for condition clauses:

<ClassName>::Subquery(
    <conditions>,
    <clauses>,
    null
    [, <substitution QQNode 1>]
    [, <substitution QQNode 2>]
    [, etc.]
)

- Subquery expansion of a query:

QQ::Expand(
    QQ::Virtual( $strVirtualName,
        <ClassName>::Subquery(
            <conditions>,
            <clauses>,
            null
            [, <substitution QQNode 1>]
            [, <substitution QQNode 2>]
            [, etc.]
        )
    )
)

Another possible improvement: if our DB engine is capable of caching queries results, it should be convenient to generate the  '_SubqueryId'  unique query id as a hash that involves the conditions and clauses of the subquery and also the substitution nodes for its named arguments. That way the aliases suffix should be always the same under the same circumstances, so queries with subqueries could also be cached.

Two things to note:

- The indentation when profiling queries with subqueries could be a visual issue (as it currently is). But I think the gain really pays the bill, compared with this minor inconvenience.

- I've chosen to suffix aliases instead of prefixing them because I presume that if some day a full subqueries implementation is developed (with no need for the “QQ to SQL” intermediate pass, and full support for subqueries as subtables inside the FROM part of the SELECT), it will use prefixes for aliases, as currently JOIN expansions do.

I hope you find this solution helpful. Standard “QQ” subqueries were still an unresolved issue.

#2  |  ‹‹ Qcodo System Message ››
Saturday, January 2, 2010, 11:06 AM PST

Fernando Lord made content edits to the issue

#3  |  Fernando Lordán (Barcelona, CAT, Spain) Spain
Wednesday, January 6, 2010, 10:40 AM PST

After having a second look at this, I've realised that my current solution only serves for subqueries that return an aggregate function. Not bad, but insufficient.

There must be a way to make a query (or at least to build an SQL statement) that returns only values from one field, not entire rows.

For a query to return only one field, a solution similar to the optional aliases suffix could be implemented. There should be enough to specify an optional BuildQuery parameter (let's call it _ResultColumn) with the QQNode or the QQAggregationClause to return. Before preparing the statement, if this parameter is present, QQueryBuilder would replace the entire QQueryBuilder->strSelectArray property with the QQNode->GetColumnAlias() or with the result of the QQAggregationClause->UpdateQueryBuilder() method. Not difficult, but it needs a while to take a look with care to not allow “illegal” QQNodes (nodes that don't refer to the query root table or expanded nodes that aren't present in the query expansion).

This leads to another big improvement not related to subqueries. It would be a great feature if regular queries could return directly values instead of row objects, thus saving a lot of memory and execution time in certain situations (and helping to solve issues #43 and #44 as a side effect). For so, the method explained in the previous paragraph could be generalized, so the _ResultColumn(s) query parameter could be an array of QQNodes and/or QQAggregationClauses.

For this last purpose, the active record InstantiateDbResult() method should also be reenginered a bit to be able to return direct values or arrays of values, instead of only full rows or arrays of full rows.

I've taken a deep look at it and I must admit that I still don't see where the QQueryBuilder->strSelectArray replacement should be done. It means working a little more, but I really see it doable with a bit of time.

Any help or thought from the QQuery system creator would be greatly appreciated. I really see this SQL abstraction as the work of a genius, and it would be great to complete it as much as possible.

#4  |  Mike Ho (San Diego, CA) United States of America Qcodo Administrator
Friday, January 8, 2010, 9:10 AM PST

Fernando, could you provide me some use case examples of some code that you would use in the front end to create a subquery?

E.g. right now you can do something like the following as a subquery

$objProjects = Project::QueryArray(
    QQ::All(),
    QQ::Expand(QQ::Virtual(
        'team_size',
        QQ::SubSql('SELECT count(*) FROM team_member_project_assn WHERE project_id={1}',
            QQN::Project()->Id)
    ))
);

What would you suggest as an pure QQ-based approach for this?

Furthermore, how would you suggest the syntax if we wanted to use team_size in a condition (e.g. instead of QQ::All(), we only want projects where team_size > 10).

#5  |  ‹‹ Qcodo System Message ››
Friday, January 8, 2010, 9:10 AM PST

Mike Ho made edits to the issue, including:

  • Status changed from New Issue to Open
#6  |  Fernando Lordán (Barcelona, CAT, Spain) Spain
Saturday, January 9, 2010, 11:54 AM PST

Sorry about my “wildcarded” examples not being clear enough in my previous explanations.

With my current solution, your subquery sample could be made this way:

<?php
$objProjects 
Project::QueryArray(
    
QQ::All(),
    
QQ::Expand(
        
QQ::Virtual(
            
'team_size',
            
Person::Subquery(
                
QQ:Equal(
                    
QQN::Person()->Project->ProjectId,
                    
QQ:NamedValue('1')
                ),                        
// Subquery conditions, they refer to the subquery QQueryBuilder context.
                
QQ::Clause(
                    
QQ::Count(
                        
QQN::Person()->Id,
                        
'person_count'    // There's no real need to name this value here as it's already named as a virtual node, but it's mandatory to assign a name to aggregated QQClauses.
                    
)
                ),                        
// Subquery clauses, they refer to the subquery QQueryBuilder context.
                
null,                    // Subquery parameters array.
                
QQN::Project()->Id        // Substitution QQNodes for named values in subquery, they refer to the main query QQueryBuilder context.
            
)
        )
    )
);
?>

And there's no problem in using the subquery QQNode for a QQCondition:

<?php
$objProjects 
Project::QueryArray(
    
QQ::GreaterThan(
        
QQ::Virtual(
            
'team_size',
            
Person::Subquery(
                
QQ:Equal(
                    
QQN::Person()->Project->ProjectId,
                    
QQ:NamedValue('1')
                ),                        
// Subquery conditions, they refer to the subquery QQueryBuilder context.
                
QQ::Clause(
                    
QQ::Count(
                        
QQN::Person()->Id,
                        
'person_count'    // There's no real need to name this value here as it's already named as a virtual node, but currently it's mandatory to assign a name to QQAggregationClauses.
                    
)
                ),                        
// Subquery clauses, they refer to the subquery QQueryBuilder context.
                
null,                    // Subquery parameters array.
                
QQN::Project()->Id        // Substitution QQNodes for named values in subquery, they refer to the main query QQueryBuilder context.
            
)
        ),
        
10
    
)
);
?>

In this last case, when the complementary solution suggested in the third message of this thread is implemented (query ability to compose SQL statements that optionally return concrete field's values instead of entire rows), then you can avoid using a named virtual QQNode and the resulting query becomes more elegant (although maybe the result of a COUNT clause is not the perfect example to illustrate the elegance of this feature).

<?php
$objProjects 
Project::QueryArray(
    
QQ::GreaterThan(
        
Person::Subquery(
            
QQ:Equal(
                
QQN::Person()->Project->ProjectId,
                
QQ:NamedValue('1')
            ),                    
// Subquery conditions, they refer to the subquery QQueryBuilder context.
            
QQ::Clause(
                
QQ::Count(
                    
QQN::Person()->Id,
                    
'person_count'
                
)
            ),                    
// Subquery clauses, they refer to the subquery QQueryBuilder context.
            
Array (
                
'_ResultColumns' => Array (
                    
QQ::Virtual('person_count')
                )                
// Columns to be returned by the subquery, they refer to the subquery QQueryBuilder context.
            
),                    // Subquery parameters array.
            
QQN::Project()->Id    // Substitution QQNodes for named values in subquery, they refer to the main query QQueryBuilder context.
        
),
        
10
    
)
);
?>

Additionally, this query ability to optionally select concrete columns values (instead of entire rows only) would really fulfill almost any subquery needs, including using subqueries inside QQConditionIn() clauses. See the next example, for instance.

#7  |  Fernando Lordán (Barcelona, CAT, Spain) Spain
Saturday, January 9, 2010, 5:16 PM PST

Going further with examples, let's take a more complex one and make the query “get all persons present in projects that have at least 5 team members younger than 35 years old which are not present in any other project” (yes, I'll suppose that persons have an “age” field to make things more interesting). I'm aware that maybe this could be made simpler using JOIN expansions, but I'm only trying to show a good example of what can be done with the suggested subqueries implementation, so let's assume we've joined a world conspiracy against convenient JOINs and let's build it using preferently subqueries.

Currently that query could be implemented this way (I've numbered table aliases after subquery nesting level, for clarity):

<?php
$objPersons 
Person::QueryArray(
    
QQ::In(
        
QQN::Person()->Project->ProjectId,
        
QQ::SubSql(
            
'SELECT
                proj1.id
             FROM
                project AS proj1
             WHERE
                (SELECT 
                    COUNT(pers2.id)
                 FROM
                    person AS pers2,
                    LEFT JOIN team_member_project_assn AS team2 ON (team2.person_id = pers2.id)
                 WHERE
                    (team2.project_id = proj1.id)
                    AND
                    (pers2.age < 35)
                    AND
                    (SELECT
                        COUNT(team3.id)
                     FROM
                        team_member_project_assn AS team3
                     WHERE
                        team3.person_id = pers2.id
                    ) = 1
                ) >= 5'
        
)
    )
);
?>

With the current subqueries implementation suggested, it could be made in pure QQ-style:

<?php
$objPersons 
Person::QueryArray(                    // Begin of main query.
    
QQ::In(
        
QQN::Person()->Project->ProjectId,
        
Project::Subquery(                            // Begin of subquery 1.
            
QQ::GreaterOrEqual(
                
Person::Subquery(                    // Begin of subquery 2.
                    
QQ::AndCondition(
                        
QQ::Equal(
                            
QQN::Person()->Project->ProjectId,
                            
QQ::NamedValue('1')
                        ),
                        
QQ::LessThan(
                            
QQN::Person()->Age,
                            
35
                        
),
                        
QQ::Equal(
                            
Project::Subquery(        // Begin of subquery 3.
                                
QQ::Equal(
                                    
QQN::Project()->Person->PersonId,
                                    
QQ::NamedValue('1')
                                ),                    
// Subquery 3 conditions, they refer to the subquery 3 QQueryBuilder context.
                                
QQ::Clause(
                                    
QQ::Count(
                                        
QQN::Project()->Id,
                                        
'count_projects_by_person'
                                    
)
                                ),                    
// Subquery 3 clauses, they refer to the subquery 3 QQueryBuilder context.
                                
Array(
                                    
'_ResultColumns' => Array(
                                        
QQ::Virtual('count_projects_by_person')
                                    )                
// Columns to be returned by subquery 3, they refer to the subquery 3 QQueryBuilder context.
                                
),                    // Subquery 3 parameters array.
                                
QQN::Person()->Id    // Substitution QQNodes for named values in subquery 3, they refer to subquery 2 QQueryBuilder context.
                            
),                        // End of subquery 3.
                            
1
                        
)
                    ),                                
// Subquery 2 conditions, they refer to the subquery 2 QQueryBuilder context.
                    
QQ::Clause(
                        
QQ::Count(
                            
QQN::Person()->Id,
                            
'count_persons_in_project'
                        
)
                    ),                                
// Subquery 2 clauses, they refer to the subquery 2 QQueryBuilder context.
                    
Array(
                        
'_ResultColumns' => Array(
                            
QQ::Virtual('count_persons_in_project')
                        )                            
// Columns to be returned by subquery 2, they refer to the subquery 2 QQueryBuilder context.
                    
),                                // Subquery 2 parameters array.
                    
QQN::Project()->Id                // Substitution QQNodes for named values in subquery 2, they refer to subquery 1 QQueryBuilder context.
                
),                                    // End of subquery 2.
                
5
            
),                                        // Subquery 1 conditions, they refer to the subquery 1 QQueryBuilder context.
            
null,                                    // Subquery 1 clauses, they refer to the subquery 1 QQueryBuilder context.
            
Array(
                
'_ResultColumns' => Array(
                    
QQN::Project()->Id
                
)                                    // Columns to be returned by subquery 1, they refer to the subquery 1 QQueryBuilder context.
            
)                                        // Subquery 1 parameters array.
        
)                                            // End of subquery 1.
    
)
);                                                    
// End of main query.
?>

Note that I can use the same names for named values in different subqueries with no risk of name collision, as different subqueries are resolved to SQL statements separately in intermediate passes.

This QQ version of the query seems way longer than the SQL version, but take into account that I've separated all the condition's pieces in different lines for clarity. Also note that improvements explained in the next messages simplify it and make it more elegant.

#8  |  Fernando Lordán (Barcelona, CAT, Spain) Spain
Saturday, January 9, 2010, 5:18 PM PST

When making the last example I've noticed the convenience of implementing an additional SubqueryCount() method in the active record class to save the need of explicitly specifying the QQ::Count() clause and the '_ResultColumns' query parameter with a QQVirtualNode in the frequent cases that subqueries are used just to count. It's exactly the same facility that the QueryCount() method currently brings to simplify count-only queries, but ported to count-only subqueries.

qcodo_query_methods.tpl

<?php

/**
 * Static Qcodo method to get a subquery node for a <%= $objTable->ClassName %>-based count.
 * Uses BuildQueryStatment to perform most of the work.
 * @param QQCondition $objConditions any conditions on the query, itself
 * @param QQClause[] $objOptionalClausees additional optional QQClause objects for this query
 * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with
 * @return string the SQL statement for the query
 */
public static function SubqueryCount(QQCondition $objConditions$objOptionalClauses null$mixParameterArray null) {
    if (!
is_array($mixParameterArray)) {
        
$mixParameterArray = Array();
    }
    if (!
array_key_exists('_SubqueryId'$mixParameterArray)) {
        
$mixParameterArray['_SubqueryId'] = uniqid('_subq_');
    }

    
// Get the subquery statement
    
try {
        
$strQuery = <%= $objTable->ClassName %>::BuildQueryStatement($objQueryBuilder$objConditions$objOptionalClauses$mixParameterArraytrue);
    } catch (
QCallerException $objExc) {
        
$objExc->IncrementOffset();
        throw 
$objExc;
    }

    
$objParentQueryNodeArray func_get_args();
    
array_shift($objParentQueryNodeArray);
    
array_shift($objParentQueryNodeArray);
    
array_shift($objParentQueryNodeArray);

    return new 
QQSubQuerySqlNode($strQuery$objParentQueryNodeArray);
}

?>

With this last improvement, my example query would be simplified like this:

<?php
$objPersons 
Person::QueryArray(                    // Begin of main query.
    
QQ::In(
        
QQN::Person()->Project->ProjectId,
        
Project::Subquery(                            // Begin of subquery 1.
            
QQ::GreaterOrEqual(
                
Person::SubqueryCount(                // Begin of subquery 2.
                    
QQ::AndCondition(
                        
QQ::Equal(
                            
QQN::Person()->Project->ProjectId,
                            
QQ::NamedValue('1')
                        ),
                        
QQ::LessThan(
                            
QQN::Person()->Age,
                            
35
                        
),
                        
QQ::Equal(
                            
Project::SubqueryCount(    // Begin of subquery 3.
                                
QQ::Equal(
                                    
QQN::Project()->Person->PersonId,
                                    
QQ::NamedValue('1')
                                ),                    
// Subquery 3 conditions, they refer to the subquery 3 QQueryBuilder context.
                                
null,                // Subquery 3 clauses, they refer to the subquery 3 QQueryBuilder context.
                                
null,                // Subquery 3 parameters array.
                                
QQN::Person()->Id    // Substitution QQNodes for named values in subquery 3, they refer to subquery 2 QQueryBuilder context.
                            
),                        // End of subquery 3.
                            
1
                        
)
                    ),                                
// Subquery 2 conditions, they refer to the subquery 2 QQueryBuilder context.
                    
null,                            // Subquery 2 clauses, they refer to the subquery 2 QQueryBuilder context.
                    
null,                            // Subquery 2 parameters array.
                    
QQN::Project()->Id                // Substitution QQNodes for named values in subquery 2, they refer to subquery 1 QQueryBuilder context.
                
),                                    // End of subquery 2.
                
5
            
),                                        // Subquery 1 conditions, they refer to the subquery 1 QQueryBuilder context.
            
null,                                    // Subquery 1 clauses, they refer to the subquery 1 QQueryBuilder context.
            
Array(
                
'_ResultColumns' => Array(
                    
QQN::Project()->Id
                
)                                    // Columns to be returned by subquery 1, they refer to the subquery 1 QQueryBuilder context.
            
)                                        // Subquery 1 parameters array.
        
)                                            // End of subquery 1.
    
)
);                                                    
// End of main query.
?>

And your original proposed sample (team_size > 10) would also be nicely simplified:

<?php
$objProjects 
Project::QueryArray(
    
QQ::GreaterThan(
        
Person::SubqueryCount(
            
QQ:Equal(
                
QQN::Person()->Project->ProjectId,
                
QQ:NamedValue('1')
            ),                    
// Subquery conditions, they refer to the subquery QQueryBuilder context.
            
null,                // Subquery clauses, they refer to the subquery QQueryBuilder context.
            
null,                // Subquery parameters array.
            
QQN::Project()->Id    // Substitution QQNodes for named values in subquery, they refer to the main query QQueryBuilder context.
        
),
        
10
    
)
);
?>
.bc
#9  |  Fernando Lordán (Barcelona, CAT, Spain) Spain
Saturday, January 9, 2010, 5:22 PM PST

By the way, I've discovered fixed aliases hardcoded in QQueryBuilder when composing COUNT-specific SQL statements, so the alias suffix substitution explained in the first comment of this thread must be applied to them to avoid alias collisions when making queries with multiple COUNT subqueries (as in my example):

QQuery.class.php

<?php
class QQueryBuilder extends QBaseClass {

[...]

    public function 
GetStatement() {
        
// SELECT Clause
        
if ($this->blnCountOnlyFlag) {
            if (
$this->blnDistinctFlag) {
                
$strSql "SELECT\r\n    COUNT(*) AS q_row_count"chr(QQNamedValue::DelimiterCode) . '{_SubqueryId}' ."\r\n" .
                    
"FROM    (SELECT DISTINCT ";
                
$strSql .= "    " implode(",\r\n    "$this->strSelectArray);
            } else
                
$strSql "SELECT\r\n    COUNT(*) AS q_row_count"chr(QQNamedValue::DelimiterCode) . '{_SubqueryId}' ."\r\n";
        } else {

[...]

        
// For Distinct Count Queries
        
if ($this->blnCountOnlyFlag && $this->blnDistinctFlag)
            
$strSql .= "\r\n) AS q_count_table"chr(QQNamedValue::DelimiterCode) . '{_SubqueryId}';

        return 
$strSql;
}

?>
.bc
#10  |  Fernando Lordán (Barcelona, CAT, Spain) Spain
Saturday, January 9, 2010, 9:03 PM PST

Another improvement: I've found out the way to overcome the limitation in the scope of named values for nested subqueries.

Previously I explained this limitation:
When using named arguments in subqueries, substitution QQNodes always inherit the QQueryBuilder from its “parent” query when being converted to column names/aliases, you can't choose to which subquery nesting level they refer to. So for example, in a query like ( Query 1 ( Subquery 2 ( Subquery 3 ) ) ), you can't substitute a named argument in (Subquery 3) with a query node from (Query 1), but only with a query node that refers to (Subquery 2).

I've found the way to solve this. Read until the end, as the solution evolves along this message and the final result is optimal.

Firstly I thought in the most easy option: you can just substitute a named value in one nesting level with a named value in the next nesting level, and go on doing this across nesting levels until you reach the subquery level where the original named value is used.

For so, the QQNamedValue must implement the GetColumnAlias() method to be able to pass its name to the SQL in the parent nesting level:

QQuery.class.php

<?php
class QQNamedValue extends QQNode {

[...]
    public function 
GetColumnAlias(QQueryBuilder $objBuilder) {
        return 
$this->Parameter(null);
    }

}
?>

With this improvement, something like this becomes possible and would work:

Class1::QueryArray(
    QQ::Equal(
        Class2::SubqueryCount(
            QQ::Equal(
                Class3::SubqueryCount(
                    QQ::AndCondition(
                        QQ::Equal(
                            QQ::Class3()->Node,
                            QQ::NamedValue('1')                // This becomes QQN::Class1()->SubstituteFromLevel1
                        ),
                        QQ::Equal(
                            Class4::SubqueryCount(
                                QQ::Equal(
                                    QQ::Class4()->Node3b,
                                    QQ::NamedValue('1')        // This becomes QQN::Class2()->SubstituteFromLevel2
                                ),
                                null,
                                null,
                                QQ::NamedValue('2')    // Substitution QQNodes for named values in subquery 4, they refer to the subquery 3 QQueryBuilder context.
                            )
                        )
                    ),
                    null,
                    null,
                    QQ::NamedValue('1'),            // Substitution QQNodes for named values in subquery 3, they refer to the subquery 2 QQueryBuilder context.
                    QQN::Class2()->SubstituteFromLevel2
                ),
                1
            ),
            null,
            null,
            QQN::Class1()->SubstituteFromLevel1        // Substitution QQNodes for named values in subquery 2, they refer to the query 1 QQueryBuilder context.
        )
    )
)

However, there are two important caveats in this approach that need to be addressed:

1.- For this to work, the BuildQueryStatement() method must allow to build SQL statements with unresolved named parameters. But only for subqueries, not for main queries.

This is really simple to solve.

qcodo_query_methods.tpl

<?php
protected static function BuildQueryStatement(&$objQueryBuilderQQCondition $objConditions$objOptionalClauses$mixParameterArray$blnCountOnly) {

[...]
    
// Ensure that there are no other unresolved named parameters if it's not a SQL subquery statement.
    
if (strpos($strQuerychr(QQNamedValue::DelimiterCode) . '{') !== false && !in_array('_SubqueryId'$mixParameterArray))
        throw new 
QCallerException('Unresolved named parameters in the query');
[...]

?>

2.- Implementing this, now the names for named values CAN collide between different subqueries. In fact they can be confused by the replacement code in complex queries if they don't preserve their name/order across nesting levels, as currently their names must always be ordered numbers which are replaced from one level to another.

For instance, this would not work as expected:

Class1::QueryArray(
    QQ::Equal(
        Class2::SubqueryCount(
            QQ::Equal(
                Class3::SubqueryCount(
                        QQ::Equal(
                            Class4::SubqueryCount(
                                QQ::AndCondition(
                                    QQ::Equal(
                                        QQ::Class4()->Node4a,
                                        QQ::NamedValue('1')        // This becomes QQN::Class1()->Substitute1
                                    ),
                                    QQ::Equal(
                                        QQ::Class4()->Node4b,
                                        QQ::NamedValue('2')        // This also becomes QQN::Class1()->Substitute1 !
                                    )
                                ),
                                null,
                                null,
                                QQ::NamedValue('1'),            // When replacing names at this level, node name '{1}' becomes parent name '{2}'...
                                QQ::NamedValue('2')                // ... and then node name '{2}' is replaced with parent name '{1}', so both named values become the same.
                            )
                        )
                    ),
                    null,
                    null,
                    QQ::NamedValue('2'),                        // Names order is scrambled at this level.
                    QQ::NamedValue('1')
                ),
                1
            ),
            null,
            null,
            QQN::Class1()->Substitute1,
            QQN::Class1()->Substitute2
        )
    )
)

In complex subqueries that pass more than one named argument from one subquery level to another, it can be difficult to track if its names are correctly ordered.

There's also a simple solution for this caveat. It consists in providing the ability to optionally specify arbitrary names for named arguments substitution, instead of necessarily using its order number as name. This way they would really become NAMED arguments, in fact, and there would be no confusion although its order was changed from one level to another.

Class1::QueryArray(
    QQ::Equal(
        Class2::SubqueryCount(
            QQ::Equal(
                Class3::SubqueryCount(
                    QQ::Equal(
                        Class4::SubqueryCount(
                            QQ::AndCondition(
                                QQ::Equal(
                                    QQ::Class4()->Node4a,
                                    QQ::NamedValue('Level3Arg1')    // This really becomes QQN::Class1()->Substitute1
                                ),
                                QQ::Equal(
                                    QQ::Class4()->Node4b,
                                    QQ::NamedValue('Level3Arg2')    // This really becomes QQN::Class1()->Substitute2
                                ),
                                QQ::Equal(
                                    QQ::Class4()->Node4c,
                                    QQ::NamedValue('1')            // This becomes QQN::Class1()->ThisSubstituteIsAlwaysTheFirstWithNumericName
                                )
                            ),
                            null,
                            Array(
                                'Level3Arg1' => QQ::NamedValue('Level2Arg2'),
                                'Level3Arg2' => QQ::NamedValue('Level2Arg1'),
                                QQ::NamedValue('1')
                            )
                        ),
                        789
                    ),
                    null,
                    Array(
                        'Level2Arg1' => QQ::NamedValue('Level1Arg2'),
                        'Level2Arg2' => QQ::NamedValue('Level1Arg1'),
                        QQ::NamedValue('1')                        // Names order is scrambled here, but now the non-numeric names avoid any confusion.
                    )
                ),
                456
            ),
            null,
            Array(
                'Level1Arg1' => QQN::Class1()->Substitute1,
                'Level1Arg2' => QQN::Class1()->Substitute2,
                QQN::Class1()->ThisSubstituteIsAlwaysTheFirstWithNumericName
            )
        ),
        123
    )
)

In fact, as confusion is avoided by replacing (deepest level first, least deep level last) arbitrary names instead of numbers, this gets really simplified and truly becomes a pure any-level nesting:

Class1::QueryArray(
    QQ::Equal(
        Class2::SubqueryCount(
            QQ::Equal(
                Class3::SubqueryCount(
                    QQ::Equal(
                        Class4::SubqueryCount(
                            QQ::AndCondition(
                                QQ::Equal(
                                    QQ::Class4()->Node4a,
                                    QQ::NamedValue('Level1Arg1')    // This really becomes QQN::Class1()->Substitute1
                                ),
                                QQ::Equal(
                                    QQ::Class4()->Node4b,
                                    QQ::NamedValue('Level1Arg2')    // This really becomes QQN::Class1()->Substitute2
                                ),
                                QQ::Equal(
                                    QQ::Class4()->Node4c,
                                    QQ::NamedValue('1')            // This really becomes QQN::Class1()->ThisSubstituteIsTheFirstWithNumericName
                                )
                            )
                        ),
                        789
                    )
                ),
                456
            ),
            null,
            Array(
                'Level1Arg1' => QQN::Class1()->Substitute1,
                'Level1Arg2' => QQN::Class1()->Substitute2
                QQN::Class1()->ThisSubstituteIsTheFirstWithNumericName
            )
        ),
        123
    )
)

Note that with this solution named parameters are specified in an array to provide the ability to specify its name in the associative key, and not as individual parameters as I've done in previous examples.

So a little change is needed in the definition of QQSubQuerySqlNode factory methods, as now named parameters are passed as one array argument:

qcodo_query_methods.tpl

<?php

/**
 * Static Qcodo method to get a subquery node for a <%= $objTable->ClassName %>-based subquery.
 * Uses BuildQueryStatment to perform most of the work.
 * @param QQCondition $objConditions any conditions on the query, itself
 * @param QQClause[] $objOptionalClausees additional optional QQClause objects for this query
 * @param QQNode[] $objParentQueryNodeArray optional array of parent QQNode objects to perform named parameters substitution in the subquery
 * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with
 * @return string the SQL statement for the query
 */
public static function Subquery(QQCondition $objConditions$objOptionalClauses null$objParentQueryNodeArray null$mixParameterArray null) {
    if (!
is_array($mixParameterArray)) {
        
$mixParameterArray = Array();
    }
    if (!
array_key_exists('_SubqueryId'$mixParameterArray)) {
        
$mixParameterArray['_SubqueryId'] = uniqid('_subq_');
    }

    
// Get the subquery statement
    
try {
        
$strQuery = <%= $objTable->ClassName %>::BuildQueryStatement($objQueryBuilder$objConditions$objOptionalClauses$mixParameterArrayfalse);
    } catch (
QCallerException $objExc) {
        
$objExc->IncrementOffset();
        throw 
$objExc;
    }

    if (
$objParentQueryNodeArray instanceof QQNode) {
        
$objParentQueryNodeArray = Array($objParentQueryNodeArray);
    } else {
        try {
            
QType::Cast($objParentQueryNodeArrayQType::ArrayType);
        } catch (
QInvalidCastException $objExc) {
            
$objExc->IncrementOffset();
            throw 
$objExc;
        }
    }
    if (!empty(
$objParentQueryNodeArray)) {
        
$objParentQueryNodeArray array_merge(Array(null), $objParentQueryNodeArray);
    }

    return new 
QQSubQuerySqlNode($strQuery$objParentQueryNodeArray);
}
    
/**
 * Static Qcodo method to get a subquery node for a <%= $objTable->ClassName %>-based count.
 * Uses BuildQueryStatment to perform most of the work.
 * @param QQCondition $objConditions any conditions on the query, itself
 * @param QQClause[] $objOptionalClausees additional optional QQClause objects for this query
 * @param QQNode[] $objParentQueryNodeArray optional array of parent QQNode objects to perform named parameters substitution in the subquery
 * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with
 * @return string the SQL statement for the query
 */
public static function SubqueryCount(QQCondition $objConditions$objOptionalClauses null$objParentQueryNodeArray null$mixParameterArray null) {
    if (!
is_array($mixParameterArray)) {
        
$mixParameterArray = Array();
    }
    if (!
array_key_exists('_SubqueryId'$mixParameterArray)) {
        
$mixParameterArray['_SubqueryId'] = uniqid('_subq_');
    }

    
// Get the subquery statement
    
try {
        
$strQuery = <%= $objTable->ClassName %>::BuildQueryStatement($objQueryBuilder$objConditions$objOptionalClauses$mixParameterArraytrue);
    } catch (
QCallerException $objExc) {
        
$objExc->IncrementOffset();
        throw 
$objExc;
    }

    if (
$objParentQueryNodeArray instanceof QQNode) {
        
$objParentQueryNodeArray = Array($objParentQueryNodeArray);
    } else {
        try {
            
QType::Cast($objParentQueryNodeArrayQType::ArrayType);
        } catch (
QInvalidCastException $objExc) {
            
$objExc->IncrementOffset();
            throw 
$objExc;
        }
    }
    if (!empty(
$objParentQueryNodeArray)) {
        
$objParentQueryNodeArray array_merge(Array(null), $objParentQueryNodeArray);
    }

    return new 
QQSubQuerySqlNode($strQuery$objParentQueryNodeArray);
}

?>

Previous behaviour is also preserved, by the way. Numeric indexes/names can still be used if the array of substitution QQNodes is not associative (note that a dummy value is appended to the beggining of the array to maintain its indexes 1-based and not zero-based). And as a facility you don't need to specify the array itself (just the QQNode) in the usual case that there's only one named parameter to specify.

And finally, the QQSubQuerySqlNode itself must respect the array keys instead of using only numeric indexes, so another little change is needed.

QQuery.class.php

<?php
class QQSubQuerySqlNode extends QQSubQueryNode {

[...]
    public function 
GetColumnAlias($objBuilder) {
        
$strSql $this->strSql;
        foreach (
$this->objParentQueryNodes as $mixIndex => $objParentQueryNode) {
            if (!
is_null($objParentQueryNode))
                
$strSql str_replace('{' $mixIndex '}'$objParentQueryNode->GetColumnAlias($objBuilder), $strSql);
        }
        return 
'(' $strSql ')';
    }
}

?>

The rest of the code already respects array keys and node names. I must say it's really fantastic to work inside this framework.

Please, ask if you have any doubts or concerns about the whole solution. The only pendant point is the improvement suggested in the third message of this thread, as it's doable but I've still not implemented it.



Copyright © 2005 - 2012, Quasidea Development, LLC
This open-source framework for PHP is released under the terms of The MIT License.