diff --git a/lib/Model.php b/lib/Model.php index 162f85b2..05940076 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -1492,25 +1492,6 @@ public function reset_dirty(): void $this->__dirty = []; } - /** - * A list of valid finder options. - * - * @var array - */ - public static array $VALID_OPTIONS = [ - 'conditions', - 'limit', - 'offset', - 'order', - 'select', - 'joins', - 'include', - 'readonly', - 'group', - 'from', - 'having' - ]; - /** * Enables the use of dynamic finders. * @@ -1622,24 +1603,118 @@ public function __call($method, $args) throw new ActiveRecordException("Call to undefined method: $method"); } + public static function select(string $columns='*'): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->select($columns); + + return $relation; + } + + /** + * @param string|array $joins + */ + public static function joins(string|array $joins): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->joins($joins); + + return $relation; + } + + public static function order(string $order): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->order($order); + + return $relation; + } + + public static function group(string $columns): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->group($columns); + + return $relation; + } + + public static function limit(int $limit): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->limit($limit); + + return $relation; + } + + public static function offset(int $offset): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->offset($offset); + + return $relation; + } + + public static function having(string $having): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->having($having); + + return $relation; + } + + public static function from(string $from): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->from($from); + + return $relation; + } + + /** + * @param string|array $include + */ + public static function include(string|array $include): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + $relation->include($include); + + return $relation; + } + + /** + * @param string|array $where + */ + public static function where(string|array $where): Relation + { + $relation = new Relation(get_called_class(), static::$alias_attribute); + + return $relation->where($where); + } + /** - * Alias for self::find('all'). + * needle is one of: * - * @see find + * array of primary keys all([1, 3, 5, 8]) WHERE author_id in (1, 3, 5, 8) + * mapping of column names all(["name"=>"Philip", "publisher"=>"Random House"]) WHERE name=Philip AND publisher=Random House + * raw WHERE statement all(['name = (?) and publisher <> (?)', 'Bill Clinton', 'Random House']) + * + * @param array $needle An array containing values for the pk * - * @return array + * @return array All the rows that matches query. If no rows match, returns [] */ - public static function all(/* ... */): array + public static function all(array $needle = []): array { - /* @phpstan-ignore-next-line */ - return static::find('all', ...func_get_args()); + $relation = new Relation(get_called_class(), static::$alias_attribute); + + return $relation->all($needle); } /** * Get a count of qualifying records. * * ``` - * YourModel::count(['conditions' => 'amount > 3.14159265']); + * YourModel::count('amount > 3.14159265'); + * YourModel::count(['name' => 'Tito', 'author_id' => 1])); * ``` * * @see find @@ -1650,7 +1725,6 @@ public static function count(/* ... */): int { $args = func_get_args(); $options = static::extract_and_validate_options($args); - $options['select'] = 'COUNT(*)'; if (!empty($args) && !is_null($args[0]) && !empty($args[0])) { if (is_hash($args[0])) { @@ -1660,13 +1734,13 @@ public static function count(/* ... */): int } } - $table = static::table(); - $sql = $table->options_to_sql($options); - $values = $sql->get_where_values(); + $relation = new Relation(get_called_class(), static::$alias_attribute); - $res = static::connection()->query_and_fetch_one($sql->to_s(), $values); + if (0 === count($options)) { + $options['conditions'] = []; + } - return $res; + return $relation->count($options['conditions']); } /** @@ -1766,22 +1840,22 @@ public static function last(/* ... */): static|null * * @throws RecordNotFound if no options are passed or finding by pk and no records matched * - * @return static|static[]|null + * @return Model|Model[]|null * * The rules for what gets returned are complex, but predictable: * * /------------------------------------------------------------------------------------------- * First Argument Return Type Example * -------------------------------------------------------------------------------------------- - * int|string static User::find(3); - * array static User::find(["name"=>"Philip"]); - * "first" static|null User::find("first", ["name"=>"Waldo"]); - * "last" static|null User::find("last", ["name"=>"William"]); - * "all" static[] User::find("all", ["name"=>"Stephen"] - * ...int|string static[] User::find(1, 3, 5, 8); - * array static[] User::find([1,3,5,8]); + * int|string Model User::find(3); + * array Model User::find(["name"=>"Philip"]); + * "first" Model|null User::find("first", ["name"=>"Waldo"]); + * "last" Model|null User::find("last", ["name"=>"William"]); + * "all" Model[] User::find("all", ["name"=>"Stephen"] + * ...int|string Model[] User::find(1, 3, 5, 8); + * array Model[] User::find([1,3,5,8]); */ - public static function find(/* $type, $options */): static|array|null + public static function find(/* $type, $options */): Model|array|null { $class = get_called_class(); @@ -1791,6 +1865,15 @@ public static function find(/* $type, $options */): static|array|null } $options = static::extract_and_validate_options($args); + $relation = new Relation(get_called_class(), static::$alias_attribute); + + foreach (Relation::$VALID_OPTIONS as $key) { + if ('conditions'!= $key && array_key_exists($key, $options)) { + $relation->$key($options[$key]); + unset($options[$key]); + } + } + $num_args = count($args); $single = true; @@ -1801,18 +1884,13 @@ public static function find(/* $type, $options */): static|array|null break; case 'last': - if (!array_key_exists('order', $options)) { - $options['order'] = join(' DESC, ', static::table()->pk) . ' DESC'; - } else { - $options['order'] = SQLBuilder::reverse_order($options['order']); - } + $relation->last(1); // fall thru - // no break case 'first': - $options['limit'] = 1; - $options['offset'] = 0; + $relation->limit(1); + $relation->offset(0); break; } @@ -1831,86 +1909,30 @@ public static function find(/* $type, $options */): static|array|null } } - // anything left in $args is a find by pk - if ($num_args > 0 && !isset($options['conditions'])) { - $list = static::find_by_pk($args, $options, true); - } else { - $options['mapped_names'] = static::$alias_attribute; - $list = static::table()->find($options); - } - - return $single ? ($list[0] ?? null) : $list; - } - - /** - * Will look up a list of primary keys from cache - * - * @param array $pks An array of primary keys - * - * @return array - */ - protected static function get_models_from_cache(array $pks): array - { - $models = []; - $table = static::table(); - - foreach ($pks as $pk) { - $options = ['conditions' => static::pk_conditions($pk)]; - $models[] = Cache::get($table->cache_key_for_model($pk), function () use ($table, $options) { - $res = $table->find($options); - - return $res ? $res[0] : []; - }, $table->cache_model_expire); - } - - return array_filter($models); - } - - /** - * Finder method which will find by a single or array of primary keys for this model. - * - * @see find - * - * @param PrimaryKey $values An array containing values for the pk - * @param array $options An options array - * - * @throws RecordNotFound if a record could not be found - * - * @return Model|Model[] - */ - public static function find_by_pk(array|string|int|null $values, array $options, bool $forceArray = false): Model|array - { - $single = !is_array($values) && !$forceArray; - if (null === $values) { - throw new RecordNotFound("Couldn't find " . get_called_class() . ' without an ID'); - } - - $table = static::table(); - - if ($table->cache_individual_model ?? false) { - $pks = is_array($values) ? $values : [$values]; - $list = static::get_models_from_cache($pks); - } else { - // int|string|array - $options['conditions'] = static::pk_conditions($values); - $list = $table->find($options); + if (is_array($args) && 0 === count($args)) { + if (array_key_exists('conditions', $options) && $single) { + $args = $options['conditions']; + if (!is_array($args)) { + $args = [$args]; + } + } else { + $args = $options; + } } - $results = count($list); - if ($results != ($expected = @count((array) $values))) { - $class = get_called_class(); - if (is_array($values)) { - $values = join(',', $values); + if ($single) { + if (1 === $num_args && is_array($args) && array_is_list($args)) { + $args = $args[0]; } - if (1 == $expected) { - throw new RecordNotFound("Couldn't find $class with ID=$values"); - } + return $relation->whereToBeReplacedByFind($args, true); + } - throw new RecordNotFound("Couldn't find all $class with IDs ($values) (found $results, but was looking for $expected)"); + if (array_key_exists('conditions', $options)) { + $args = $options; } - return $single ? $list[0] : $list; + return $relation->all($args); } /** @@ -1954,12 +1976,12 @@ public static function is_options_hash(mixed $options, bool $throw = true): bool { if (is_hash($options)) { $keys = array_keys($options); - $diff = array_diff($keys, self::$VALID_OPTIONS); + $diff = array_diff($keys, Relation::$VALID_OPTIONS); if (!empty($diff) && $throw) { throw new ActiveRecordException('Unknown key(s): ' . join(', ', $diff)); } - $intersect = array_intersect($keys, self::$VALID_OPTIONS); + $intersect = array_intersect($keys, Relation::$VALID_OPTIONS); if (!empty($intersect)) { return true; diff --git a/lib/Relation.php b/lib/Relation.php new file mode 100644 index 00000000..4412c870 --- /dev/null +++ b/lib/Relation.php @@ -0,0 +1,475 @@ + + */ + public static array $VALID_OPTIONS = [ + 'conditions', + 'limit', + 'offset', + 'order', + 'select', + 'joins', + 'include', + 'readonly', + 'group', + 'from', + 'having' + ]; + + /** + * @var array + */ + private array $alias_attribute; + + /** + * @var class-string + */ + private string $className; + + private ?Table $tableImpl = null; + + /** + * @var array + */ + private array $options = []; + + /** + * @param class-string $className + * @param array $alias_attribute + */ + public function __construct(string $className, array $alias_attribute) + { + $this->alias_attribute = $alias_attribute; + $this->className = $className; + } + + public function __get(string $name): mixed + { + if ('last' === $name) { + return $this->last(1); + } + if ('find' === $name) { + return $this->find(); + } + + return $this; + } + + private function table(): Table + { + if (null === $this->tableImpl) { + $this->tableImpl = Table::load($this->className); + } + + return $this->tableImpl; + } + + public function last(int $limit): Relation + { + $this->limit($limit); + + if (array_key_exists('order', $this->options)) { + if (str_contains($this->options['order'], join(' ASC, ', $this->table()->pk) . ' ASC')) { + $this->options['order'] = SQLBuilder::reverse_order((string) $this->options['order']); + } + } else { + $this->options['order'] = join(' DESC, ', $this->table()->pk) . ' DESC'; + } + + return $this; + } + + public function select(string $columns): Relation + { + $this->options['select'] = $columns; + + return $this; + } + + /** + * @param string|array $joins + */ + public function joins(string|array $joins): Relation + { + $this->options['joins'] = $joins; + + return $this; + } + + public function order(string $order): Relation + { + $this->options['order'] = $order; + + return $this; + } + + public function limit(int $limit): Relation + { + $this->options['limit'] = $limit; + + return $this; + } + + public function group(string $columns): Relation + { + $this->options['group'] = $columns; + + return $this; + } + + public function offset(int $offset): Relation + { + $this->options['offset'] = $offset; + + return $this; + } + + public function having(string $having): Relation + { + $this->options['having'] = $having; + + return $this; + } + + public function from(string $from): Relation + { + $this->options['from'] = $from; + + return $this; + } + + /** + * @param string|array $include + */ + public function include(string|array $include): Relation + { + $this->options['include'] = $include; + + return $this; + } + + /** + * @param string|array $where + */ + public function where(string|array $where): Relation + { + if (array_key_exists('where', $this->options)) { + array_push($this->options['where'], $where); + } else { + $this->options['where'] = [$where]; + } + + return $this; + } + + public function readonly(bool $readonly): Relation + { + $this->options['readonly'] = $readonly; + + return $this; + } + + /** + * Implementation of Ruby On Rails finder + * + * @see https://api.rubyonrails.org/v7.0.7.2/classes/ActiveRecord/FinderMethods.html#method-i-find + * + * Person.find(1) # returns the object for ID = 1 + * Person.find("1") # returns the object for ID = 1 + * Person.find(999999) # throws RecordNotFound + * Person.find("can't be casted to int") # throws TypeError + * + * Person.find([1, 2]) # returns an array for objects with IDs in (7, 17) + * Person.find([1, -999]) # throws RecordNotFound + * Person.find([1]) # returns an array for the object with ID = 1 + * Person.find([-11]) # returns an empty array + * + * Person.where('name = Bob').find() # executes the where statement + * Person.find() # throws ValidationsArgumentError as there's no where statement to execute + * + * @param int|string|array $id + * + * @throws RecordNotFound if any of the records cannot be found + * + * @return Model|array|null See above + */ + public function find(int|string|array $id = null): Model|null|array + { + if (array_key_exists('where', $this->options)) { + $values = $this->buildWhereSQL($this->options['where']); + + if (null !== $id) { + $values[0] = "({$values[0]}) AND "; + if (is_array($id)) { + $values[0] .= $this->table()->pk[0] . ' in (' . implode(',', $id) . ')'; + } else { + $values[0] .= $this->table()->pk[0] . ' = ' . $id; + } + } + $this->options['conditions'] = $values; + + $this->options['mapped_names'] = $this->alias_attribute; + $list = $this->table()->find($this->options); + + unset($this->options['conditions']); + } else { + if (null === $id) { + throw new ValidationsArgumentError('Cannot call find() without where() being first specified'); + } + + unset($this->options['mapped_names']); + $list = $this->find_by_pk($id, true); + unset($this->options['conditions']); + } + + if (null === $id || is_array($id)) { + return $list; + } + if (0 === count($list)) { + return null; + } + + return $list[0]; + } + + /** + * @param array|string> $fragments + * + * @return array + */ + private function buildWhereSQL(array $fragments): array + { + $templates = []; + $values = []; + + foreach ($fragments as $fragment) { + if (is_array($fragment)) { + if (is_hash($fragment)) { + foreach ($fragment as $key => $value) { + array_push($templates, "{$key} = (?)"); + array_push($values, $value); + } + } else { + array_push($templates, array_shift($fragment)); + if (count($fragment) > 0) { + $values = array_merge($values, $fragment); + } + } + } else { + array_push($templates, $fragment); + } + } + + array_unshift($values, implode(' AND ', $templates)); + + return $values; + } + + /** + * needle is one of: + * + * primary key value where(3) WHERE author_id=3 + * mapping of column names where(["name"=>"Philip", "publisher"=>"Random House"]) finds the first row of WHERE name=Philip AND publisher=Random House + * raw WHERE statement where(['name = (?) and publisher <> (?)', 'Bill Clinton', 'Random House']) + * + * @param int|string|array $needle + * @param bool $isUsingOriginalFind true if called from version 1 find, false otherwise + * + * @return Model|null The single row that matches query. If no rows match, returns null + */ + public function whereToBeReplacedByFind(int|string|array $needle, bool $isUsingOriginalFind = false): Model|null + { + $this->limit(1); + + if (is_array($needle)) { + if (!array_key_exists('conditions', $this->options) && count($needle) > 0) { + $this->options['conditions'] = $needle; + } + $this->options['mapped_names'] = $this->alias_attribute; + $list = $this->table()->find($this->options); + } else { + unset($this->options['mapped_names']); + $list = $this->find_by_pk($needle, $isUsingOriginalFind); + } + + if (null == $list) { + return null; + } + + return $list[0]; + } + + /** + * needle is one of: + * + * empty array all() returns all rows in the database + * array of primary keys all([1, 3]) WHERE author_id in (1, 3) + * mapping of column names all(["name"=>"Philip", "publisher"=>"Random House"]) WHERE name=Philip AND publisher=Random House + * raw WHERE statement all(['name = (?) and publisher <> (?)', 'Bill Clinton', 'Random House']) + * + * @param array $needle An array containing values for the pk + * + * @return array All the rows that matches query. If no rows match, returns [] + */ + public function all(array $needle = []): array + { + $list = []; + + // Only for backwards compatibility with version 1 API + $isUsingOriginalFind = false; + foreach (self::$VALID_OPTIONS as $key) { + if (array_key_exists($key, $needle)) { + $this->options[$key] = $needle[$key]; + unset($needle[$key]); + $isUsingOriginalFind = true; + } + } + + if (array_is_list($needle) && count($needle) > 0 && !$this->isRawWhereStatement($needle)) { + unset($this->options['mapped_names']); + + return $this->find_by_pk($needle, $isUsingOriginalFind); + } + + if (!$isUsingOriginalFind) { + $this->options['conditions'] = $needle; + } + $this->options['mapped_names'] = $this->alias_attribute; + + return $this->table()->find($this->options); + } + + /** + * Hack until functionality of where is moved into find + * + * @param array $pieces + */ + private function isRawWhereStatement(array $pieces): bool + { + return str_contains($pieces[0], '(?)'); + } + + /** + * Get a count of qualifying records. + * + * ``` + * count() + * count('amount > 3.14159265'); + * count('author_id=3') + * count(['name' => 'Tito', 'author_id' => 1])); + * count(['author_id' => [1, 2])); + * count(['author_id' => 1]); + * count(['author_id=? and name=?', 1, 'Tito']); + * ``` + * + * @see find + * + * @param mixed $where The qualifications for a row to be counted + * + * @return int Number of records that matched the query + */ + public function count(mixed $where = []): int + { + $this->options['conditions'] = $where; + + $this->select('COUNT(*)'); + $sql = $this->table()->options_to_sql($this->options); + $values = $sql->get_where_values(); + + $res = $this->table()->conn->query_and_fetch_one($sql->to_s(), $values); + + return $res; + } + + /** + * Finder method which will find by a single or array of primary keys for this model. + * + * @see find + * + * @param mixed $values An array containing values for the pk + * @param bool $throwErrorIfNotFound True if version 1 behavior, false is version 2 + * + * @throws RecordNotFound if a record could not be found + * + * @return array + */ + private function find_by_pk(mixed $values, bool $throwErrorIfNotFound): array + { + if ($this->table()->cache_individual_model ?? false) { + $pks = is_array($values) ? $values : [$values]; + $list = $this->get_models_from_cache($pks); + } else { + $this->options['conditions'] = $this->pk_conditions($values); + $list = $this->table()->find($this->options); + } + $results = count($list); + + if ($results != ($expected = @count((array) $values))) { + $class = get_called_class(); + if (is_array($values)) { + $values = implode(',', $values); + } + + if (1 == $expected && !$throwErrorIfNotFound) { + return []; + } + + throw new RecordNotFound("Couldn't find all $class with IDs ($values) (found $results, but was looking for $expected)"); + } + + return $list; + } + + /** + * Will look up a list of primary keys from cache + * + * @param array $pks An array of primary keys + * + * @return array + */ + public function get_models_from_cache(array $pks): array + { + $models = []; + $table = $this->table(); + + foreach ($pks as $pk) { + $options = ['conditions' => $this->pk_conditions($pk)]; + $models[] = Cache::get($table->cache_key_for_model($pk), function () use ($table, $options) { + $res = $table->find($options); + + return $res ? $res[0] : null; + }, $table->cache_model_expire); + } + + return array_filter($models); + } + + /** + * Returns a hash containing the names => values of the primary key. + * + * @param int|array $args Primary key value(s) + * + * @return array + */ + private function pk_conditions(int|array $args): array + { + $ret = [$this->table()->pk[0] => $args]; + + return $ret; + } +} diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index eda53493..cf65270a 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -12,6 +12,7 @@ use ActiveRecord\Inflector; use ActiveRecord\Model; use ActiveRecord\Reflections; +use ActiveRecord\Relation; use ActiveRecord\SQLBuilder; use ActiveRecord\Table; use ActiveRecord\Utils; @@ -278,7 +279,7 @@ protected function merge_association_options(array $options): array protected function unset_non_finder_options(array $options): array { foreach (array_keys($options) as $option) { - if (!in_array($option, Model::$VALID_OPTIONS)) { + if (!in_array($option, Relation::$VALID_OPTIONS)) { unset($options[$option]); } } diff --git a/test/ActiveRecordFindTest.php b/test/ActiveRecordFindTest.php index dd93f9b3..fbadb4e6 100644 --- a/test/ActiveRecordFindTest.php +++ b/test/ActiveRecordFindTest.php @@ -155,7 +155,7 @@ public function testFindByPkArrayWithOptions() public function testFindNothingWithSqlInString() { - $this->expectException(RecordNotFound::class); + $this->expectException(TypeError::class); Author::first('name = 123123123'); } @@ -512,7 +512,7 @@ public function testFindByZero() public function testFindByNull() { - $this->expectException(RecordNotFound::class); + $this->expectException(TypeError::class); Author::find(null); } diff --git a/test/RelationTest.php b/test/RelationTest.php new file mode 100644 index 00000000..201a4941 --- /dev/null +++ b/test/RelationTest.php @@ -0,0 +1,228 @@ +find(1); + $this->assertEquals('Tito', $query->name); + $this->assertEquals(['sharks' => 'lasers'], $query->return_something()); + + $query = $rel->find('1'); + $this->assertEquals('Tito', $query->name); + $queries = $rel->find([1, 2]); + $this->assertEquals(2, count($queries)); + $this->assertEquals('Tito', $queries[0]->name); + $this->assertEquals('George W. Bush', $queries[1]->name); + + $queries = $rel->find([1]); + $this->assertEquals(1, count($queries)); + $this->assertEquals('Tito', $queries[0]->name); + } + + public function testFindSingleArrayElementNotFound() + { + $this->expectException(RecordNotFound::class); + $queries = Author::select('name')->find([999999]); + } + + public function testFindNotAllArrayElementsFound() + { + $this->expectException(RecordNotFound::class); + $queries = Author::select('name')->find([1, 999999]); + } + + public function testFindWrongType() + { + $this->expectException(TypeError::class); + Author::select('name')->find('not a number'); + } + + public function testFindSingleElementNotFound() + { + $this->expectException(RecordNotFound::class); + $query = Author::select('name')->find(99999); + } + + public function testFindNoWhere() + { + $this->expectException(ValidationsArgumentError::class); + $query = Author::select('name')->find; + } + + public function testWhere() + { + $queries = Author::select('name')->where("mixedCaseField = 'Bill'")->find; + $this->assertEquals(2, count($queries)); + $this->assertEquals('Bill Clinton', $queries[0]->name); + $this->assertEquals('Uncle Bob', $queries[1]->name); + + $queries = Author::select('name')->where(['name = (?)', 'Bill Clinton'])->find; + $this->assertEquals(1, count($queries)); + $this->assertEquals('Bill Clinton', $queries[0]->name); + $queries = Author::select('name')->where(['name = (?)', 'Not found'])->find; + $this->assertEquals(0, count($queries)); + + $queries = Author::select('name')->where(['mixedCaseField'=>'Bill'])->find; + $this->assertEquals(2, count($queries)); + $this->assertEquals('Bill Clinton', $queries[0]->name); + $this->assertEquals('Uncle Bob', $queries[1]->name); + } + + public function testWhereWithPrimaryKey() + { + $rel = Author::select('name')->where(["mixedCaseField = 'Bill'"]); + $queries = $rel->find([3, 4]); + $this->assertEquals(2, count($queries)); + $this->assertEquals('Bill Clinton', $queries[0]->name); + $this->assertEquals('Uncle Bob', $queries[1]->name); + + $queries = $rel->find([4, 999999]); + $this->assertEquals(1, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + + $queries = $rel->find([999998, 999999]); + $this->assertEquals(0, count($queries)); + + $query = $rel->find(999999); + $this->assertNull($query); + + $query = $rel->find(4); + $this->assertEquals('Uncle Bob', $query->name); + } + + public function testWhereOrder() + { + $relation = Author::select('name')->where("mixedCaseField = 'Bill'"); + + $queries = $relation->last->find; + $this->assertEquals(1, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + + $queries = $relation->last(1)->find; + $this->assertEquals(1, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + + $queries = $relation->last(2)->find; + $this->assertEquals(2, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + $this->assertEquals('Bill Clinton', $queries[1]->name); + + $queries = $relation->last(1)->last(2)->find; + $this->assertEquals(2, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + $this->assertEquals('Bill Clinton', $queries[1]->name); + + $queries = Author::order('parent_author_id DESC')->where(['mixedCaseField'=>'Bill'])->find; + $this->assertEquals(2, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + $this->assertEquals('Bill Clinton', $queries[1]->name); + } + + public function testWhereAnd() + { + $queries = Author::select('name')->where(['mixedCaseField'=>'Bill', 'parent_author_id'=>1])->find; + $this->assertEquals('Bill Clinton', $queries[0]->name); + + $queries = Author::select('name')->where(['mixedCaseField'=>'Bill', 'parent_author_id'=>2])->find; + $this->assertEquals('Uncle Bob', $queries[0]->name); + + $queries = Author::select('name')->where(['mixedCaseField = (?) and parent_author_id <> (?)', 'Bill', 1])->find; + $this->assertEquals('Uncle Bob', $queries[0]->name); + + $queries = Author::select('name') + ->where(['mixedCaseField = (?)', 'Bill']) + ->where(['parent_author_id = (?)', 1]) + ->where("author_id = '3'") + ->where(['mixedCaseField'=>'Bill', 'name'=>'Bill Clinton']) + ->find; + $this->assertEquals(1, count($queries)); + $this->assertEquals('Bill Clinton', $queries[0]->name); + } + + public function testWhereChained() + { + $relation = Author::select('authors.author_id, authors.name'); + $relation = Author::joins(['LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)']); + $relation = Author::order('name DESC'); + $relation = Author::limit(2); + $relation = Author::group('name'); + $relation = Author::offset(2); + $query = $relation->where(3); + + $query = Author::select('name') + ->order('name DESC') + ->limit(2) + ->group('name') + ->offset(2) + ->having('length(name) = 2') + ->readonly(true) + ->from('books') + ->where(['name' => 'Bill Clinton']) + ->find(3); + $this->assertEquals(null, $query); + } + + public function testAllNoParameters() + { + $queries = Author::all(); + $this->assertEquals(4, count($queries)); + } + + public function testAllPrimaryKeys() + { + $queries = Author::all([1, 3]); + $this->assertEquals(2, count($queries)); + $this->assertEquals('Tito', $queries[0]->name); + $this->assertEquals('Bill Clinton', $queries[1]->name); + } + + public function testAllAnd() + { + $queries = Author::all(['mixedCaseField'=>'Bill', 'parent_author_id'=>1]); + $this->assertEquals(1, count($queries)); + $this->assertEquals('Bill Clinton', $queries[0]->name); + + $queries = Author::all(['mixedCaseField'=>'Bill', 'parent_author_id'=>2]); + $this->assertEquals(1, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + + $queries = Author::all(['mixedCaseField = (?) and parent_author_id <> (?)', 'Bill', 1]); + $this->assertEquals(1, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + } + + public function testAllLast() + { + $relation = Author::select('name'); + + $queries = $relation->last(2)->all(['mixedCaseField'=>'Bill']); + $this->assertEquals(2, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + $this->assertEquals('Bill Clinton', $queries[1]->name); + + $queries = $relation->last(2)->last(1)->all(['mixedCaseField'=>'Bill']); + $this->assertEquals(1, count($queries)); + $this->assertEquals('Uncle Bob', $queries[0]->name); + } + + public function testAllChained() + { + $queries = Author::select('name') + ->order('name DESC') + ->limit(2) + ->group('name') + ->offset(2) + ->having('length(name) = 2') + ->from('books') + ->readonly(true) + ->all([3]); + $this->assertEquals(0, count($queries)); + } +} diff --git a/test/fixtures/authors.csv b/test/fixtures/authors.csv index ea124cf2..2c1f6ac7 100644 --- a/test/fixtures/authors.csv +++ b/test/fixtures/authors.csv @@ -2,4 +2,4 @@ author_id,parent_author_id,name,mixedCaseField 1,3,"Tito","Tito" 2,2,"George W. Bush","George W." 3,1,"Bill Clinton","Bill" -4,2,"Uncle Bob","Uncle Bob" \ No newline at end of file +4,2,"Uncle Bob","Bill" \ No newline at end of file