* @changes 1.0.0b47 Fixed the new version of ::precount() to work with tables having an explicit schema [wb, 2012-09-21]

* @changes 1,0,0b46 Fixed a bug with ::precount() not working for self-joining tables [wb, 2012-09-16]

* @changes 1.0.0b45 Added support for the starts with like, `^~`, and ends with like, `$~`, operators to both ::build() and ::filter() [wb, 2011-06-20]

* @changes 1.0.0b44 Backwards Compatibility Break - ::sort() and ::sortByCallback() now return a new fRecordSet instead of sorting the record set in place [wb, 2011-06-20]

* @changes 1.0.0b43 Added the ability to pass SQL and values to ::buildFromSQL(), added the ability to manually pass the `$limit` and `$page` to ::buildFromArray() and ::buildFromSQL(), changed ::slice() to remember `$limit` and `$page` if possible when `$remember_original_count` is `TRUE` [wb, 2011-01-11]

* @changes 1.0.0b23 Added an extra parameter to ::diff(), ::filter(), ::intersect(), ::slice() and ::unique() to save the number of records in the current set as the non-limited count for the new set [wb, 2009-09-15]

* 'column|column2|column3~' => VALUE // (column LIKE '%VALUE%' OR column2 LIKE '%VALUE%' OR column3 LIKE '%VALUE%')

* 'column|column2|column3~' => array(VALUE, VALUE2, ... ) // ((column LIKE '%VALUE%' OR column2 LIKE '%VALUE%' OR column3 LIKE '%VALUE%') AND (column LIKE '%VALUE2%' OR column2 LIKE '%VALUE2%' OR column3 LIKE '%VALUE2%') AND ... )

* }}}

*

* When creating a condition in the form `column|column2|column3~`, if the

* value for the condition is a single string that contains spaces, the

* string will be parsed for search terms. The search term parsing will

* handle quoted phrases and normal words and will strip punctuation and

* stop words (such as "the" and "a").

*

* The order bys array can contain `key => value` entries in any of the

* @param string|array $sql The SQL to create the set from, or an array of the SQL statement plus values to escape

* @param string|array $non_limited_count_sql An SQL statement, or an array of the SQL statement plus values to escape, to get the total number of rows that would have been returned if a `LIMIT` clause had not been used. Should only be passed if a `LIMIT` clause is used in `$sql`.

* @param integer $limit The number of records the SQL statement was limited to - this is information only and does not affect the SQL

* @param integer $page The page of records the SQL statement returned - this is information only and does not affect the SQL

* @param fRecordSet|array|fActiveRecord $records The record set, array of records, or record to merge with the current record set, duplicates will **not** be removed

* @return fRecordSet The merged record sets

*/

publicfunctionmerge($records)

{

$classes=array_flip((array) $this->class);

if ($recordsinstanceoffRecordSet) {

$new_records=$records->records;

$classes+=array_flip((array) $records->class);

} elseif (is_array($records)) {

$new_records=array();

foreach ($recordsas$record) {

if (!$recordinstanceoffActiveRecord) {

thrownewfProgrammerException(

'One of the records specified is not an instance of %s',

'fActiveRecord'

);

}

$new_records[] =$record;

$classes[get_class($record)] =TRUE;

}

} elseif ($recordsinstanceoffActiveRecord) {

$new_records=array($records);

$classes[get_class($records)] =TRUE;

} else {

thrownewfProgrammerException(

'The records specified, %1$s, are invalid. Must be an %2$s, %3$s or an array of %4$s.',

$records,

'fRecordSet',

'fActiveRecord',

'fActiveRecords'

);

}

if (!$new_records) {

return$this;

}

returnnewfRecordSet(

array_keys($classes),

array_merge(

$this->records,

$new_records

)

);

}

/**

* Checks to see if an offset exists

*

* This method is required by the ArrayAccess interface.

*

* @internal

*

* @param mixed $offset The offset to check

* @return boolean If the offset exists

*/

publicfunctionoffsetExists($offset)

{

returnisset($this->records[$offset]);

}

/**

* Returns a record based on the offset

*

* This method is required by the ArrayAccess interface.

*

* @internal

*

* @throws fNoRemainingException When the offset specified is beyond the last record

*

* @param mixed $offset The offset of the record to get

* @return fActiveRecord The requested record

*/

publicfunctionoffsetGet($offset)

{

if ((!is_integer($offset) &&!is_numeric($offset)) ||$offset<0) {

thrownewfProgrammerException(

'The offset specified, %1$s, is invalid. Offsets must be a non-negative integer.',

$offset

);

}

if ($offset>=count($this->records)) {

thrownewfNoRemainingException(

'The offset specified, %1$s, is beyond the last record in the set',

$offset

);

}

return$this->records[$offset];

}

/**

* Prevents setting values to the record set

*

* This method is required by the ArrayAccess interface.

*

* @internal

*

* @param mixed $offset The offset to set

* @param mixed $value The value to set to the offset

* @return void

*/

publicfunctionoffsetSet($offset, $value)

{

thrownewfProgrammerException(

'%1$s does not allow setting records via array syntax',

'fRecordSet'

);

}

/**

* Prevents unsetting values from the record set

*

* This method is required by the ArrayAccess interface.

*

* @internal

*

* @param mixed $offset The offset to unset

* @return void

*/

publicfunctionoffsetUnset($offset)

{

thrownewfProgrammerException(

'%1$s does not allow unsetting records via array syntax',

'fRecordSet'

);

}

/**

* Builds the related records for all records in this set in one DB query

*

* @param string $related_class This should be the name of a related class

* @param string $route This should be a column name or a join table name and is only required when there are multiple routes to a related table. If there are multiple routes and this is not specified, an fProgrammerException will be thrown.

* @return fRecordSet The record set object, to allow for method chaining

// If it is a straight join, keep track of the value by the related column value

} else {

$method='get'.fGrammar::camelize($relationship['column'], TRUE);

$keys[$relationship['related_column']] =$record->$method();

}

// Loop through and find each row for the current record

$rows=array();

try {

while (!array_diff_assoc($keys, $result->current())) {

$row=$result->fetchRow();

// If we are going through a join table we need to remove the related primary key that was used for matching

if (isset($relationship['join_table'])) {

unset($row[$relationship['column']]);

}

$rows[] =$row;

}

} catch (fExpectedException$e) { }

// Set up the result object for the new record set

$set=newfRecordSet($related_class, newArrayIterator($rows));

// Inject the new record set into the record

$method='inject'.fGrammar::pluralize($related_class);

$record->$method($set, $route);

}

return$this;

}

/**

* Counts the related records for all records in this set in one DB query

*

* @param string $related_class This should be the name of a related class

* @param string $route This should be a column name or a join table name and is only required when there are multiple routes to a related table. If there are multiple routes and this is not specified, an fProgrammerException will be thrown.

* @return fRecordSet The record set object, to allow for method chaining

* Creates the objects for related records that are in a one-to-one or many-to-one relationship with the current class in a single DB query

*

* @param string $related_class This should be the name of a related class

* @param string $route This should be the column name of the foreign key and is only required when there are multiple routes to a related table. If there are multiple routes and this is not specified, an fProgrammerException will be thrown.

* @return fRecordSet The record set object, to allow for method chaining

* - The result of the last call plus the next record for the second and subsequent calls

*

* {{{

* #!php

* function my_reduce($sum, $record)

* {

* return $sum + $record->getQuantity();

* )

* // For the first record, 0.0 will be passed as the $sum, then subsequent

* // calls end up getting the return value of the last call to my_reduce()

* $total_quantity = $record_set->reduce('my_reduce', 0.0);

* }}}

*

* @param callback $callback The callback to pass the records to - see method description for details

* @param mixed $initial_value The initial value to seed reduce with

* @return mixed The result of the reduce operation

*/

publicfunctionreduce($callback, $initial_value=NULL)

{

if (!$this->records) {

return$initial_value;

}

$result=$initial_value;

if (is_string($callback) &&strpos($callback, '::') !==FALSE) {

$callback=explode('::', $callback);

}

foreach($this->recordsas$record) {

$result=call_user_func($callback, $result, $record);

}

return$result;

}

/**

* Slices a section of records from the set and returns a new set containing those

*

* @param integer $offset The index to start at, negative indexes will slice that many records from the end

* @param integer $length The number of records to return, negative values will stop that many records before the end, `NULL` will return all records to the end of the set - if there are not enough records, less than `$length` will be returned

* @param boolean $remember_original_count If the number of records in the current set should be saved as the non-limited count for the new set - the page will be reset to `1` either way