From 6af3a0e42bc5b7a404242d7a21f49351eae2985d Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 22 Feb 2014 19:21:32 +0100 Subject: [PATCH] SQL overhaul: introducing tree-like queries Reason: until now, PostSearchService was using magic to get around the biggest limitation of SqlQuery.php: it didn't support arbitrary order of operations. You couldn't join with something and tell then to select something from it. Additionally, forging UPDATE queries was a joke. The new Sql* classes replace SqlQuery completely and address these issues. Using Sql* classes might be tedious and ugly at times, but it is necessary step to improve model layer maintainability. It is by no menas complete implementation of SQL grammar, but for current needs it's enough, and, what's most important, it is easily extensible. Additional changes: * Added sorting style aliases - fav_count - tag_count - comment_count * Sorting by multiple tokens in post search is now possible * Searching for disliked posts with "special:disliked" always yields results (even if user has disabled showing disliked posts by default) * More maintainable next/prev post support --- src/Controllers/IndexController.php | 16 +- src/Controllers/PostController.php | 16 +- src/Database.php | 28 +- src/Models/AbstractCrudModel.php | 37 +- src/Models/CommentModel.php | 33 +- src/Models/Entities/PostEntity.php | 33 +- src/Models/Entities/TagEntity.php | 10 +- src/Models/Entities/UserEntity.php | 56 +-- src/Models/PostModel.php | 132 +++-- src/Models/PropertyModel.php | 37 +- .../SearchServices/AbstractSearchService.php | 32 +- .../SearchServices/CommentSearchService.php | 17 +- .../SearchServices/PostSearchService.php | 455 ++++++++---------- .../SearchServices/TagSearchService.php | 49 +- .../SearchServices/UserSearchService.php | 17 +- src/Models/TagModel.php | 127 +++-- src/Models/TokenModel.php | 24 +- src/Models/UserModel.php | 126 ++--- .../BinaryOperators/SqlAdditionOperator.php | 8 + .../BinaryOperators/SqlAliasOperator.php | 13 + .../BinaryOperators/SqlEqualsOperator.php | 8 + .../SqlEqualsOrGreaterOperator.php | 8 + .../SqlEqualsOrLesserOperator.php | 8 + .../BinaryOperators/SqlGreaterOperator.php | 8 + .../BinaryOperators/SqlLesserOperator.php | 8 + .../BinaryOperators/SqlLikeOperator.php | 8 + .../SqlSubtractionOperator.php | 8 + .../NullaryOperators/SqlNullOperator.php | 8 + .../NullaryOperators/SqlRandomOperator.php | 11 + src/Sql/Operators/SqlBinaryOperator.php | 19 + src/Sql/Operators/SqlNullaryOperator.php | 4 + src/Sql/Operators/SqlOperator.php | 4 + src/Sql/Operators/SqlUnaryOperator.php | 25 + src/Sql/Operators/SqlVariableOperator.php | 38 ++ .../UnaryOperators/SqlAbsOperator.php | 8 + .../UnaryOperators/SqlCountOperator.php | 8 + .../UnaryOperators/SqlExistsOperator.php | 8 + .../UnaryOperators/SqlIsNullOperator.php | 8 + .../UnaryOperators/SqlNegationOperator.php | 8 + .../UnaryOperators/SqlNoCaseOperator.php | 8 + .../VariableOperators/SqlConjunction.php | 16 + .../VariableOperators/SqlDisjunction.php | 16 + .../VariableOperators/SqlInOperator.php | 23 + src/Sql/SqlBinding.php | 31 ++ src/Sql/SqlExpression.php | 45 ++ src/Sql/SqlStringExpression.php | 15 + src/Sql/Statements/SqlDeleteStatement.php | 39 ++ src/Sql/Statements/SqlInsertStatement.php | 62 +++ src/Sql/Statements/SqlRawStatement.php | 15 + src/Sql/Statements/SqlSelectStatement.php | 197 ++++++++ src/Sql/Statements/SqlStatement.php | 4 + src/Sql/Statements/SqlUpdateStatement.php | 53 ++ src/SqlQuery.php | 99 ---- src/Views/layout-normal.phtml | 2 +- upgrade.php | 11 +- 55 files changed, 1345 insertions(+), 762 deletions(-) create mode 100644 src/Sql/Operators/BinaryOperators/SqlAdditionOperator.php create mode 100644 src/Sql/Operators/BinaryOperators/SqlAliasOperator.php create mode 100644 src/Sql/Operators/BinaryOperators/SqlEqualsOperator.php create mode 100644 src/Sql/Operators/BinaryOperators/SqlEqualsOrGreaterOperator.php create mode 100644 src/Sql/Operators/BinaryOperators/SqlEqualsOrLesserOperator.php create mode 100644 src/Sql/Operators/BinaryOperators/SqlGreaterOperator.php create mode 100644 src/Sql/Operators/BinaryOperators/SqlLesserOperator.php create mode 100644 src/Sql/Operators/BinaryOperators/SqlLikeOperator.php create mode 100644 src/Sql/Operators/BinaryOperators/SqlSubtractionOperator.php create mode 100644 src/Sql/Operators/NullaryOperators/SqlNullOperator.php create mode 100644 src/Sql/Operators/NullaryOperators/SqlRandomOperator.php create mode 100644 src/Sql/Operators/SqlBinaryOperator.php create mode 100644 src/Sql/Operators/SqlNullaryOperator.php create mode 100644 src/Sql/Operators/SqlOperator.php create mode 100644 src/Sql/Operators/SqlUnaryOperator.php create mode 100644 src/Sql/Operators/SqlVariableOperator.php create mode 100644 src/Sql/Operators/UnaryOperators/SqlAbsOperator.php create mode 100644 src/Sql/Operators/UnaryOperators/SqlCountOperator.php create mode 100644 src/Sql/Operators/UnaryOperators/SqlExistsOperator.php create mode 100644 src/Sql/Operators/UnaryOperators/SqlIsNullOperator.php create mode 100644 src/Sql/Operators/UnaryOperators/SqlNegationOperator.php create mode 100644 src/Sql/Operators/UnaryOperators/SqlNoCaseOperator.php create mode 100644 src/Sql/Operators/VariableOperators/SqlConjunction.php create mode 100644 src/Sql/Operators/VariableOperators/SqlDisjunction.php create mode 100644 src/Sql/Operators/VariableOperators/SqlInOperator.php create mode 100644 src/Sql/SqlBinding.php create mode 100644 src/Sql/SqlExpression.php create mode 100644 src/Sql/SqlStringExpression.php create mode 100644 src/Sql/Statements/SqlDeleteStatement.php create mode 100644 src/Sql/Statements/SqlInsertStatement.php create mode 100644 src/Sql/Statements/SqlRawStatement.php create mode 100644 src/Sql/Statements/SqlSelectStatement.php create mode 100644 src/Sql/Statements/SqlStatement.php create mode 100644 src/Sql/Statements/SqlUpdateStatement.php delete mode 100644 src/SqlQuery.php diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index 05016c7d..cefeee2b 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -54,14 +54,14 @@ class IndexController private function featureNewPost() { - $query = (new SqlQuery) - ->select('id') - ->from('post') - ->where('type = ?')->put(PostType::Image) - ->and('safety = ?')->put(PostSafety::Safe) - ->orderBy($this->config->main->dbDriver == 'sqlite' ? 'random()' : 'rand()') - ->desc(); - $featuredPostId = Database::fetchOne($query)['id']; + $stmt = (new SqlSelectStatement) + ->setColumn('id') + ->setTable('post') + ->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('type', new SqlBinding(PostType::Image))) + ->add(new SqlEqualsOperator('safety', new SqlBinding(PostSafety::Safe)))) + ->setOrderBy(new SqlRandomOperator(), SqlSelectStatement::ORDER_DESC); + $featuredPostId = Database::fetchOne($stmt)['id']; if (!$featuredPostId) return null; diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php index 55dff495..41de7069 100644 --- a/src/Controllers/PostController.php +++ b/src/Controllers/PostController.php @@ -427,17 +427,17 @@ class PostController try { $this->context->transport->lastSearchQuery = InputHelper::get('last-search-query'); - $prevPostQuery = $this->context->transport->lastSearchQuery . ' prev:' . $id; - $nextPostQuery = $this->context->transport->lastSearchQuery . ' next:' . $id; - $prevPost = current(PostSearchService::getEntities($prevPostQuery, 1, 1)); - $nextPost = current(PostSearchService::getEntities($nextPostQuery, 1, 1)); + list ($prevPostId, $nextPostId) = + PostSearchService::getPostIdsAround( + $this->context->transport->lastSearchQuery, $id); } #search for some reason was invalid, e.g. tag was deleted in the meantime catch (Exception $e) { $this->context->transport->lastSearchQuery = ''; - $prevPost = current(PostSearchService::getEntities('prev:' . $id, 1, 1)); - $nextPost = current(PostSearchService::getEntities('next:' . $id, 1, 1)); + list ($prevPostId, $nextPostId) = + PostSearchService::getPostIdsAround( + $this->context->transport->lastSearchQuery, $id); } PostSearchService::enableTokenLimit(true); @@ -449,8 +449,8 @@ class PostController $this->context->score = $score; $this->context->flagged = $flagged; $this->context->transport->post = $post; - $this->context->transport->prevPostId = $prevPost ? $prevPost->id : null; - $this->context->transport->nextPostId = $nextPost ? $nextPost->id : null; + $this->context->transport->prevPostId = $prevPostId ? $prevPostId : null; + $this->context->transport->nextPostId = $nextPostId ? $nextPostId : null; } diff --git a/src/Database.php b/src/Database.php index 7e0cad7c..3e528dbb 100644 --- a/src/Database.php +++ b/src/Database.php @@ -24,19 +24,19 @@ class Database } } - public static function makeStatement(SqlQuery $sqlQuery) + protected static function makeStatement(SqlStatement $stmt) { try { - $stmt = self::$pdo->prepare($sqlQuery->getSql()); + $pdoStatement = self::$pdo->prepare($stmt->getAsString()); + foreach ($stmt->getBindings() as $key => $value) + $pdoStatement->bindValue(is_numeric($key) ? $key + 1 : ltrim($key, ':'), $value); } catch (Exception $e) { - throw new Exception('Problem with ' . $sqlQuery->getSql() . ' (' . $e->getMessage() . ')'); + throw new Exception('Problem with ' . $stmt->getAsString() . ' (' . $e->getMessage() . ')'); } - foreach ($sqlQuery->getBindings() as $key => $value) - $stmt->bindValue(is_numeric($key) ? $key + 1 : $key, $value); - return $stmt; + return $pdoStatement; } public static function disconnect() @@ -49,32 +49,32 @@ class Database return self::$pdo !== null; } - public static function query(SqlQuery $sqlQuery) + public static function exec(SqlStatement $stmt) { if (!self::connected()) throw new Exception('Database is not connected'); - $statement = self::makeStatement($sqlQuery); + $statement = self::makeStatement($stmt); try { $statement->execute(); } catch (Exception $e) { - throw new Exception('Problem with ' . $sqlQuery->getSql() . ' (' . $e->getMessage() . ')'); + throw new Exception('Problem with ' . $stmt->getAsString() . ' (' . $e->getMessage() . ')'); } - self::$queries []= $sqlQuery; + self::$queries []= $stmt; return $statement; } - public static function fetchOne(SqlQuery $sqlQuery) + public static function fetchOne(SqlStatement $stmt) { - $statement = self::query($sqlQuery); + $statement = self::exec($stmt); return $statement->fetch(); } - public static function fetchAll(SqlQuery $sqlQuery) + public static function fetchAll(SqlStatement $stmt) { - $statement = self::query($sqlQuery); + $statement = self::exec($stmt); return $statement->fetchAll(); } diff --git a/src/Models/AbstractCrudModel.php b/src/Models/AbstractCrudModel.php index 39ac4603..20d107fc 100644 --- a/src/Models/AbstractCrudModel.php +++ b/src/Models/AbstractCrudModel.php @@ -21,12 +21,12 @@ abstract class AbstractCrudModel implements IModel public static function findById($key, $throw = true) { - $query = (new SqlQuery) - ->select('*') - ->from(static::getTableName()) - ->where('id = ?')->put($key); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('*'); + $stmt->setTable(static::getTableName()); + $stmt->setCriterion(new SqlEqualsOperator('id', new SqlBinding($key))); - $row = Database::fetchOne($query); + $row = Database::fetchOne($stmt); if ($row) return self::convertRow($row); @@ -37,12 +37,12 @@ abstract class AbstractCrudModel implements IModel public static function findByIds(array $ids) { - $query = (new SqlQuery) - ->select('*') - ->from(static::getTableName()) - ->where('id')->in()->genSlots($ids)->put($ids); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('*'); + $stmt->setTable(static::getTableName()); + $stmt->setCriterion(SqlInOperator::fromArray('id', SqlBinding::fromArray($ids))); - $rows = Database::fetchAll($query); + $rows = Database::fetchAll($stmt); if ($rows) return self::convertRows($rows); @@ -51,9 +51,10 @@ abstract class AbstractCrudModel implements IModel public static function getCount() { - $query = new SqlQuery(); - $query->select('count(1)')->as('count')->from(static::getTableName()); - return Database::fetchOne($query)['count']; + $stmt = new SqlSelectStatement(); + $stmt->setColumn(new SqlAliasOperator(new SqlCountOperator('1'), 'count')); + $stmt->setTable(static::getTableName()); + return Database::fetchOne($stmt)['count']; } @@ -106,13 +107,9 @@ abstract class AbstractCrudModel implements IModel throw new Exception('Can be run only within transaction'); if (!$entity->id) { - $config = \Chibi\Registry::getConfig(); - $query = (new SqlQuery); - if ($config->main->dbDriver == 'sqlite') - $query->insertInto($table)->defaultValues(); - else - $query->insertInto($table)->values()->open()->close(); - Database::query($query); + $stmt = new SqlInsertStatement(); + $stmt->setTable($table); + Database::exec($stmt); $entity->id = Database::lastInsertId(); } } diff --git a/src/Models/CommentModel.php b/src/Models/CommentModel.php index 3b482d32..1bd8a40f 100644 --- a/src/Models/CommentModel.php +++ b/src/Models/CommentModel.php @@ -25,13 +25,14 @@ class CommentModel extends AbstractCrudModel 'comment_date' => $comment->commentDate, 'commenter_id' => $comment->commenterId]; - $query = (new SqlQuery) - ->update('comment') - ->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) - ->put(array_values($bindings)) - ->where('id = ?')->put($comment->id); + $stmt = new SqlUpdateStatement(); + $stmt->setTable('comment'); + $stmt->setCriterion(new SqlEqualsOperator('id', new SqlBinding($comment->id))); - Database::query($query); + foreach ($bindings as $key => $val) + $stmt->setColumn($key, new SqlBinding($val)); + + Database::exec($stmt); }); } @@ -39,10 +40,10 @@ class CommentModel extends AbstractCrudModel { Database::transaction(function() use ($comment) { - $query = (new SqlQuery) - ->deleteFrom('comment') - ->where('id = ?')->put($comment->id); - Database::query($query); + $stmt = new SqlDeleteStatement(); + $stmt->setTable('comment'); + $stmt->setCriterion(new SqlEqualsOperator('id', new SqlBinding($comment->id))); + Database::exec($stmt); }); } @@ -50,14 +51,12 @@ class CommentModel extends AbstractCrudModel public static function findAllByPostId($key) { - $query = new SqlQuery(); - $query - ->select('comment.*') - ->from('comment') - ->where('post_id = ?') - ->put($key); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('comment.*'); + $stmt->setTable('comment'); + $stmt->setCriterion(new SqlEqualsOperator('post_id', new SqlBinding($key))); - $rows = Database::fetchAll($query); + $rows = Database::fetchAll($stmt); if ($rows) return self::convertRows($rows); return []; diff --git a/src/Models/Entities/PostEntity.php b/src/Models/Entities/PostEntity.php index 9db2d78f..8ff6d5b9 100644 --- a/src/Models/Entities/PostEntity.php +++ b/src/Models/Entities/PostEntity.php @@ -51,12 +51,12 @@ class PostEntity extends AbstractEntity { if ($this->hasCache('favoritee')) return $this->getCache('favoritee'); - $query = (new SqlQuery) - ->select('user.*') - ->from('user') - ->innerJoin('favoritee')->on('favoritee.user_id = user.id') - ->where('favoritee.post_id = ?')->put($this->id); - $rows = Database::fetchAll($query); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('user.*'); + $stmt->setTable('user'); + $stmt->addInnerJoin('favoritee', new SqlEqualsOperator('favoritee.user_id', 'user.id')); + $stmt->setCriterion(new SqlEqualsOperator('favoritee.post_id', new SqlBinding($this->id))); + $rows = Database::fetchAll($stmt); $favorites = UserModel::convertRows($rows); $this->setCache('favoritee', $favorites); return $favorites; @@ -69,13 +69,20 @@ class PostEntity extends AbstractEntity if ($this->hasCache('relations')) return $this->getCache('relations'); - $query = (new SqlQuery) - ->select('post.*') - ->from('post') - ->innerJoin('crossref') - ->on()->open()->raw('post.id = crossref.post2_id')->and('crossref.post_id = ?')->close()->put($this->id) - ->or()->open()->raw('post.id = crossref.post_id')->and('crossref.post2_id = ?')->close()->put($this->id); - $rows = Database::fetchAll($query); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('post.*'); + $stmt->setTable('post'); + $binding = new SqlBinding($this->id); + $stmt->addInnerJoin('crossref', (new SqlDisjunction) + ->add( + (new SqlConjunction) + ->add(new SqlEqualsOperator('post.id', 'crossref.post2_id')) + ->add(new SqlEqualsOperator('crossref.post_id', $binding))) + ->add( + (new SqlConjunction) + ->add(new SqlEqualsOperator('post.id', 'crossref.post_id')) + ->add(new SqlEqualsOperator('crossref.post2_id', $binding)))); + $rows = Database::fetchAll($stmt); $posts = PostModel::convertRows($rows); $this->setCache('relations', $posts); return $posts; diff --git a/src/Models/Entities/TagEntity.php b/src/Models/Entities/TagEntity.php index 13647580..07d93203 100644 --- a/src/Models/Entities/TagEntity.php +++ b/src/Models/Entities/TagEntity.php @@ -5,10 +5,10 @@ class TagEntity extends AbstractEntity public function getPostCount() { - $query = (new SqlQuery) - ->select('count(*)')->as('count') - ->from('post_tag') - ->where('tag_id = ?')->put($this->id); - return Database::fetchOne($query)['count']; + $stmt = new SqlSelectStatement(); + $stmt->setColumn(new SqlAliasOperator(new SqlCountOperator('1'), 'count')); + $stmt->setTable('post_tag'); + $stmt->setCriterion(new SqlEqualsOperator('tag_id', new SqlBinding($this->id))); + return Database::fetchOne($stmt)['count']; } } diff --git a/src/Models/Entities/UserEntity.php b/src/Models/Entities/UserEntity.php index 4eb87c02..21870795 100644 --- a/src/Models/Entities/UserEntity.php +++ b/src/Models/Entities/UserEntity.php @@ -111,22 +111,24 @@ class UserEntity extends AbstractEntity public function hasFavorited($post) { - $query = (new SqlQuery) - ->select('count(1)')->as('count') - ->from('favoritee') - ->where('user_id = ?')->put($this->id) - ->and('post_id = ?')->put($post->id); - return Database::fetchOne($query)['count'] == 1; + $stmt = new SqlSelectStatement(); + $stmt->setColumn(new SqlAliasOperator(new SqlCountOperator('1'), 'count')); + $stmt->setTable('favoritee'); + $stmt->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('user_id', new SqlBinding($this->id))) + ->add(new SqlEqualsOperator('post_id', new SqlBinding($post->id)))); + return Database::fetchOne($stmt)['count'] == 1; } public function getScore($post) { - $query = (new SqlQuery) - ->select('score') - ->from('post_score') - ->where('user_id = ?')->put($this->id) - ->and('post_id = ?')->put($post->id); - $row = Database::fetchOne($query); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('score'); + $stmt->setTable('post_score'); + $stmt->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('user_id', new SqlBinding($this->id))) + ->add(new SqlEqualsOperator('post_id', new SqlBinding($post->id)))); + $row = Database::fetchOne($stmt); if ($row) return intval($row['score']); return null; @@ -134,28 +136,28 @@ class UserEntity extends AbstractEntity public function getFavoriteCount() { - $sqlQuery = (new SqlQuery) - ->select('count(1)')->as('count') - ->from('favoritee') - ->where('user_id = ?')->put($this->id); - return Database::fetchOne($sqlQuery)['count']; + $stmt = new SqlSelectStatement(); + $stmt->setColumn(new SqlAliasOperator(new SqlCountOperator('1'), 'count')); + $stmt->setTable('favoritee'); + $stmt->setCriterion(new SqlEqualsOperator('user_id', new SqlBinding($this->id))); + return Database::fetchOne($stmt)['count']; } public function getCommentCount() { - $sqlQuery = (new SqlQuery) - ->select('count(1)')->as('count') - ->from('comment') - ->where('commenter_id = ?')->put($this->id); - return Database::fetchOne($sqlQuery)['count']; + $stmt = new SqlSelectStatement(); + $stmt->setColumn(new SqlAliasOperator(new SqlCountOperator('1'), 'count')); + $stmt->setTable('comment'); + $stmt->setCriterion(new SqlEqualsOperator('commenter_id', new SqlBinding($this->id))); + return Database::fetchOne($stmt)['count']; } public function getPostCount() { - $sqlQuery = (new SqlQuery) - ->select('count(1)')->as('count') - ->from('post') - ->where('uploader_id = ?')->put($this->id); - return Database::fetchOne($sqlQuery)['count']; + $stmt = new SqlSelectStatement(); + $stmt->setColumn(new SqlAliasOperator(new SqlCountOperator('1'), 'count')); + $stmt->setTable('post'); + $stmt->setCriterion(new SqlEqualsOperator('uploader_id', new SqlBinding($this->id))); + return Database::fetchOne($stmt)['count']; } } diff --git a/src/Models/PostModel.php b/src/Models/PostModel.php index f18f739e..848f1788 100644 --- a/src/Models/PostModel.php +++ b/src/Models/PostModel.php @@ -48,48 +48,50 @@ class PostModel extends AbstractCrudModel 'source' => $post->source, ]; - $query = (new SqlQuery) - ->update('post') - ->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) - ->put(array_values($bindings)) - ->where('id = ?')->put($post->id); - Database::query($query); + $stmt = new SqlUpdateStatement(); + $stmt->setTable('post'); + + foreach ($bindings as $key => $value) + $stmt->setColumn($key, new SqlBinding($value)); + + $stmt->setCriterion(new SqlEqualsOperator('id', new SqlBinding($post->id))); + Database::exec($stmt); //tags $tags = $post->getTags(); - $query = (new SqlQuery) - ->deleteFrom('post_tag') - ->where('post_id = ?')->put($post->id); - Database::query($query); + $stmt = new SqlDeleteStatement(); + $stmt->setTable('post_tag'); + $stmt->setCriterion(new SqlEqualsOperator('post_id', new SqlBinding($post->id))); + Database::exec($stmt); foreach ($tags as $postTag) { - $query = (new SqlQuery) - ->insertInto('post_tag') - ->surround('post_id, tag_id') - ->values()->surround('?, ?') - ->put([$post->id, $postTag->id]); - Database::query($query); + $stmt = new SqlInsertStatement(); + $stmt->setTable('post_tag'); + $stmt->setColumn('post_id', new SqlBinding($post->id)); + $stmt->setColumn('tag_id', new SqlBinding($postTag->id)); + Database::exec($stmt); } //relations $relations = $post->getRelations(); - $query = (new SqlQuery) - ->deleteFrom('crossref') - ->where('post_id = ?')->put($post->id) - ->or('post2_id = ?')->put($post->id); - Database::query($query); + $stmt = new SqlDeleteStatement(); + $stmt->setTable('crossref'); + $binding = new SqlBinding($post->id); + $stmt->setCriterion((new SqlDisjunction) + ->add(new SqlEqualsOperator('post_id', $binding)) + ->add(new SqlEqualsOperator('post2_id', $binding))); + Database::exec($stmt); foreach ($relations as $relatedPost) { - $query = (new SqlQuery) - ->insertInto('crossref') - ->surround('post_id, post2_id') - ->values()->surround('?, ?') - ->put([$post->id, $relatedPost->id]); - Database::query($query); + $stmt = new SqlInsertStatement(); + $stmt->setTable('crossref'); + $stmt->setColumn('post_id', new SqlBinding($post->id)); + $stmt->setColumn('post2_id', new SqlBinding($relatedPost->id)); + Database::exec($stmt); } }); } @@ -98,36 +100,31 @@ class PostModel extends AbstractCrudModel { Database::transaction(function() use ($post) { - $queries = []; + $binding = new SqlBinding($post->id); - $queries []= (new SqlQuery) - ->deleteFrom('post_score') - ->where('post_id = ?')->put($post->id); + $stmt = new SqlDeleteStatement(); + $stmt->setTable('post_score'); + $stmt->setCriterion(new SqlEqualsOperator('post_id', $binding)); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->deleteFrom('post_tag') - ->where('post_id = ?')->put($post->id); + $stmt->setTable('post_tag'); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->deleteFrom('crossref') - ->where('post_id = ?')->put($post->id) - ->or('post2_id = ?')->put($post->id); + $stmt->setTable('favoritee'); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->deleteFrom('favoritee') - ->where('post_id = ?')->put($post->id); + $stmt->setTable('comment'); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->update('comment') - ->set('post_id = NULL') - ->where('post_id = ?')->put($post->id); + $stmt->setTable('crossref'); + $stmt->setCriterion((new SqlDisjunction) + ->add(new SqlEqualsOperator('post_id', $binding)) + ->add(new SqlEqualsOperator('post_id', $binding))); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->deleteFrom('post') - ->where('id = ?')->put($post->id); - - foreach ($queries as $query) - Database::query($query); + $stmt->setTable('post'); + $stmt->setCriterion(new SqlEqualsOperator('id', $binding)); + Database::exec($stmt); }); } @@ -136,12 +133,12 @@ class PostModel extends AbstractCrudModel public static function findByName($key, $throw = true) { - $query = (new SqlQuery) - ->select('*') - ->from('post') - ->where('name = ?')->put($key); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('*'); + $stmt->setTable('post'); + $stmt->setCriterion(new SqlEqualsOperator('name', new SqlBinding($key))); - $row = Database::fetchOne($query); + $row = Database::fetchOne($stmt); if ($row) return self::convertRow($row); @@ -161,12 +158,12 @@ class PostModel extends AbstractCrudModel public static function findByHash($key, $throw = true) { - $query = (new SqlQuery) - ->select('*') - ->from('post') - ->where('file_hash = ?')->put($key); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('*'); + $stmt->setTable('post'); + $stmt->setCriterion(new SqlEqualsOperator('file_hash', new SqlBinding($key))); - $row = Database::fetchOne($query); + $row = Database::fetchOne($stmt); if ($row) return self::convertRow($row); @@ -192,12 +189,13 @@ class PostModel extends AbstractCrudModel } $postIds = array_keys($postMap); - $sqlQuery = (new SqlQuery) - ->select('tag.*, post_id') - ->from('tag') - ->innerJoin('post_tag')->on('post_tag.tag_id = tag.id') - ->where('post_id')->in()->genSlots($postIds)->put($postIds); - $rows = Database::fetchAll($sqlQuery); + $stmt = new SqlSelectStatement(); + $stmt->setTable('tag'); + $stmt->addColumn('tag.*'); + $stmt->addColumn('post_id'); + $stmt->addInnerJoin('post_tag', new SqlEqualsOperator('post_tag.tag_id', 'tag.id')); + $stmt->setCriterion(SqlInOperator::fromArray('post_id', SqlBinding::fromArray($postIds))); + $rows = Database::fetchAll($stmt); foreach ($rows as $row) { diff --git a/src/Models/PropertyModel.php b/src/Models/PropertyModel.php index 31e4eaa9..67e9754a 100644 --- a/src/Models/PropertyModel.php +++ b/src/Models/PropertyModel.php @@ -20,8 +20,10 @@ class PropertyModel implements IModel { self::$loaded = true; self::$allProperties = []; - $query = (new SqlQuery())->select('*')->from('property'); - foreach (Database::fetchAll($query) as $row) + $stmt = new SqlSelectStatement(); + $stmt ->setColumn('*'); + $stmt ->setTable('property'); + foreach (Database::fetchAll($stmt) as $row) self::$allProperties[$row['prop_id']] = $row['value']; } } @@ -39,33 +41,26 @@ class PropertyModel implements IModel self::loadIfNecessary(); Database::transaction(function() use ($propertyId, $value) { - $row = Database::query((new SqlQuery) - ->select('id') - ->from('property') - ->where('prop_id = ?') - ->put($propertyId)); - - $query = (new SqlQuery); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('id'); + $stmt->setTable('property'); + $stmt->setCriterion(new SqlEqualsOperator('prop_id', new SqlBinding($propertyId))); + $row = Database::fetchOne($stmt); if ($row) { - $query - ->update('property') - ->set('value = ?') - ->put($value) - ->where('prop_id = ?') - ->put($propertyId); + $stmt = new SqlUpdateStatement(); + $stmt->setCriterion(new SqlEqualsOperator('prop_id', new SqlBinding($propertyId))); } else { - $query - ->insertInto('property') - ->open()->raw('prop_id, value_id')->close() - ->open()->raw('?, ?')->close() - ->put([$propertyId, $value]); + $stmt = new SqlInsertStatement(); + $stmt->setColumn('prop_id', new SqlBinding($propertyId)); } + $stmt->setTable('property'); + $stmt->setColumn('value', new SqlBinding($value)); - Database::query($query); + Database::exec($stmt); self::$allProperties[$propertyId] = $value; }); diff --git a/src/Models/SearchServices/AbstractSearchService.php b/src/Models/SearchServices/AbstractSearchService.php index 64f91ffd..af674c28 100644 --- a/src/Models/SearchServices/AbstractSearchService.php +++ b/src/Models/SearchServices/AbstractSearchService.php @@ -8,17 +8,18 @@ abstract class AbstractSearchService return $modelClassName; } - protected static function decorate(SqlQuery $sqlQuery, $searchQuery) + protected static function decorate(SqlSelectStatement $stmt, $searchQuery) { throw new NotImplementedException(); } - protected static function decoratePager(SqlQuery $sqlQuery, $perPage, $page) + protected static function decoratePager(SqlSelectStatement $stmt, $perPage, $page) { if ($perPage === null) return; - $sqlQuery->limit('?')->put($perPage); - $sqlQuery->offset('?')->put(($page - 1) * $perPage); + $stmt->setLimit( + new SqlBinding($perPage), + new SqlBinding(($page - 1) * $perPage)); } public static function getEntitiesRows($searchQuery, $perPage = null, $page = 1) @@ -26,12 +27,12 @@ abstract class AbstractSearchService $modelClassName = self::getModelClassName(); $table = $modelClassName::getTableName(); - $sqlQuery = new SqlQuery(); - $sqlQuery->select($table . '.*'); - static::decorate($sqlQuery, $searchQuery); - self::decoratePager($sqlQuery, $perPage, $page); + $stmt = new SqlSelectStatement(); + $stmt->setColumn($table . '.*'); + static::decorate($stmt, $searchQuery); + static::decoratePager($stmt, $perPage, $page); - $rows = Database::fetchAll($sqlQuery); + $rows = Database::fetchAll($stmt); return $rows; } @@ -47,12 +48,13 @@ abstract class AbstractSearchService $modelClassName = self::getModelClassName(); $table = $modelClassName::getTableName(); - $sqlQuery = new SqlQuery(); - $sqlQuery->select('count(1)')->as('count'); - $sqlQuery->from()->raw('(')->select('1'); - static::decorate($sqlQuery, $searchQuery); - $sqlQuery->raw(')'); + $innerStmt = new SqlSelectStatement(); + static::decorate($innerStmt, $searchQuery); - return Database::fetchOne($sqlQuery)['count']; + $stmt = new SqlSelectStatement(); + $stmt->setColumn(new SqlAliasOperator(new SqlCountOperator('1'), 'count')); + $stmt->setSource($innerStmt); + + return Database::fetchOne($stmt)['count']; } } diff --git a/src/Models/SearchServices/CommentSearchService.php b/src/Models/SearchServices/CommentSearchService.php index ac6a7e48..c779ac1d 100644 --- a/src/Models/SearchServices/CommentSearchService.php +++ b/src/Models/SearchServices/CommentSearchService.php @@ -1,21 +1,14 @@ from('comment') - ->innerJoin('post') - ->on('post_id = post.id'); + $stmt->setTable('comment'); + $stmt->addInnerJoin('post', new SqlEqualsOperator('post_id', 'post.id')); $allowedSafety = PrivilegesHelper::getAllowedSafety(); - if (empty($allowedSafety)) - $sqlQuery->where('0'); - else - $sqlQuery->where('post.safety')->in()->genSlots($allowedSafety)->put($allowedSafety); + $stmt->setCriterion(SqlInOperator::fromArray('post.safety', SqlBinding::fromArray($allowedSafety))); - $sqlQuery - ->orderBy('comment.id') - ->desc(); + $stmt->addOrderBy('comment.id', SqlSelectStatement::ORDER_DESC); } } diff --git a/src/Models/SearchServices/PostSearchService.php b/src/Models/SearchServices/PostSearchService.php index 17b11619..bd8a88f0 100644 --- a/src/Models/SearchServices/PostSearchService.php +++ b/src/Models/SearchServices/PostSearchService.php @@ -3,108 +3,148 @@ class PostSearchService extends AbstractSearchService { private static $enableTokenLimit = true; + public static function getPostIdsAround($searchQuery, $postId) + { + return Database::transaction(function() use ($searchQuery, $postId) + { + $stmt = new SqlRawStatement('CREATE TEMPORARY TABLE IF NOT EXISTS post_search(id INTEGER PRIMARY KEY, post_id INTEGER)'); + Database::exec($stmt); + + $stmt = new SqlDeleteStatement(); + $stmt->setTable('post_search'); + Database::exec($stmt); + + $innerStmt = new SqlSelectStatement($searchQuery); + $innerStmt->setColumn('id'); + self::decorate($innerStmt, $searchQuery); + $stmt = new SqlInsertStatement(); + $stmt->setTable('post_search'); + $stmt->setSource(['post_id'], $innerStmt); + Database::exec($stmt); + + $stmt = new SqlSelectStatement(); + $stmt->setColumn('post_id'); + $stmt->setTable('post_search'); + $stmt->setCriterion( + new SqlEqualsOrLesserOperator( + new SqlAbsOperator( + new SqlSubtractionOperator('id', (new SqlSelectStatement) + ->setTable('post_search') + ->setColumn('id') + ->setCriterion(new SqlEqualsOperator('post_id', new SqlBinding($postId))))), + 1)); + $rows = Database::fetchAll($stmt); + $ids = array_map(function($row) { return $row['post_id']; }, $rows); + + if (count($ids) == 1 or // no prev and no next post + count($ids) == 0) // even the post we are looking at is hidden from normal search for whatever reason + { + return [null, null]; + } + elseif (count($ids) == 2) // no prev or no next post + { + return $ids[0] == $postId + ? [$ids[1], null] + : [null, $ids[0]]; + } + elseif (count($ids) == 3) // both prev and next post + { + return [$ids[2], $ids[0]]; + } + else + { + throw new Exception('Unexpected result count (ids: ' . join(',', $ids) . ')'); + } + }); + } + public static function enableTokenLimit($enable) { self::$enableTokenLimit = $enable; } - protected static function filterUserSafety(SqlQuery $sqlQuery) + protected static function decorateNegation(SqlExpression $criterion, $negative) + { + return !$negative + ? $criterion + : new SqlNegationOperator($criterion); + } + + protected static function filterUserSafety(SqlSelectStatement $stmt) { $allowedSafety = PrivilegesHelper::getAllowedSafety(); - if (empty($allowedSafety)) - $sqlQuery->raw('0'); - else - $sqlQuery->raw('safety')->in()->genSlots($allowedSafety)->put($allowedSafety); + $stmt->getCriterion()->add(SqlInOperator::fromArray('safety', SqlBinding::fromArray($allowedSafety))); } - protected static function filterChain(SqlQuery $sqlQuery) - { - if (isset($sqlQuery->__chained)) - $sqlQuery->and(); - else - $sqlQuery->where(); - $sqlQuery->__chained = true; - } - - protected static function filterNegate(SqlQuery $sqlQuery) - { - $sqlQuery->not(); - } - - protected static function filterTag($sqlQuery, $val) + protected static function filterTag(SqlSelectStatement $stmt, $val, $neg) { $tag = TagModel::findByName($val); - $sqlQuery - ->exists() - ->open() - ->select('1') - ->from('post_tag') - ->where('post_id = post.id') - ->and('post_tag.tag_id = ?')->put($tag->id) - ->close(); + $innerStmt = new SqlSelectStatement(); + $innerStmt->setTable('post_tag'); + $innerStmt->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('post_tag.tag_id', new SqlBinding($tag->id)))); + $stmt->getCriterion()->add(self::decorateNegation(new SqlExistsOperator($innerStmt), $neg)); } - protected static function filterTokenId($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenId($val) { $ids = preg_split('/[;,]/', $val); $ids = array_map('intval', $ids); - if (empty($ids)) - $sqlQuery->raw('0'); - else - $sqlQuery->raw('id')->in()->genSlots($ids)->put($ids); + return SqlInOperator::fromArray('id', $ids); } - protected static function filterTokenIdMin($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenIdMin($val) { - $sqlQuery->raw('id >= ?')->put(intval($val)); + return new SqlEqualsOrGreaterOperator('id', new SqlBinding(intval($val))); } - protected static function filterTokenIdMax($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenIdMax($val) { - $sqlQuery->raw('id <= ?')->put(intval($val)); + return new SqlEqualsOrLesserOperator('id', new SqlBinding(intval($val))); } - protected static function filterTokenScoreMin($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenScoreMin($val) { - $sqlQuery->raw('score >= ?')->put(intval($val)); + return new SqlEqualsOrGreaterOperator('score', new SqlBinding(intval($val))); } - protected static function filterTokenScoreMax($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenScoreMax($val) { - $sqlQuery->raw('score <= ?')->put(intval($val)); + return new SqlEqualsOrLesserOperator('score', new SqlBinding(intval($val))); } - protected static function filterTokenTagMin($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenTagMin($val) { - $sqlQuery->raw('tag_count >= ?')->put(intval($val)); + return new SqlEqualsOrGreaterOperator('tag_count', new SqlBinding(intval($val))); } - protected static function filterTokenTagMax($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenTagMax($val) { - $sqlQuery->raw('tag_count <= ?')->put(intval($val)); + return new SqlEqualsOrLesserOperator('tag_count', new SqlBinding(intval($val))); } - protected static function filterTokenFavMin($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenFavMin($val) { - $sqlQuery->raw('fav_count >= ?')->put(intval($val)); + return new SqlEqualsOrGreaterOperator('fav_count', new SqlBinding(intval($val))); } - protected static function filterTokenFavMax($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenFavMax($val) { - $sqlQuery->raw('fav_count <= ?')->put(intval($val)); + return new SqlEqualsOrLesserOperator('fav_count', new SqlBinding(intval($val))); } - protected static function filterTokenCommentMin($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenCommentMin($val) { - $sqlQuery->raw('comment_count >= ?')->put(intval($val)); + return new SqlEqualsOrGreaterOperator('comment_count', new SqlBinding(intval($val))); } - protected static function filterTokenCommentMax($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenCommentMax($val) { - $sqlQuery->raw('comment_count <= ?')->put(intval($val)); + return new SqlEqualsOrLesserOperator('comment_count', new SqlBinding(intval($val))); } - protected static function filterTokenSpecial($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenSpecial($val) { $context = \Chibi\Registry::getContext(); @@ -112,40 +152,33 @@ class PostSearchService extends AbstractSearchService { case 'liked': case 'likes': - $sqlQuery - ->exists() - ->open() - ->select('1') - ->from('post_score') - ->where('post_id = post.id') - ->and('score > 0') - ->and('user_id = ?')->put($context->user->id) - ->close(); - break; + $innerStmt = new SqlSelectStatement(); + $innerStmt->setTable('post_score'); + $innerStmt->setCriterion((new SqlConjunction) + ->add(new SqlGreaterOperator('score', '0')) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('user_id', new SqlBinding($context->user->id)))); + return new SqlExistsOperator($innerStmt); case 'disliked': case 'dislikes': - $sqlQuery - ->exists() - ->open() - ->select('1') - ->from('post_score') - ->where('post_id = post.id') - ->and('score < 0') - ->and('user_id = ?')->put($context->user->id) - ->close(); - break; + $innerStmt = new SqlSelectStatement(); + $innerStmt->setTable('post_score'); + $innerStmt->setCriterion((new SqlConjunction) + ->add(new SqlLesserOperator('score', '0')) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('user_id', new SqlBinding($context->user->id)))); + return new SqlExistsOperator($innerStmt); case 'hidden': - $sqlQuery->raw('hidden'); - break; + return new SqlStringExpression('hidden'); default: throw new SimpleException('Unknown special "' . $val . '"'); } } - protected static function filterTokenType($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenType($val) { switch ($val) { @@ -162,7 +195,7 @@ class PostSearchService extends AbstractSearchService default: throw new SimpleException('Unknown type "' . $val . '"'); } - $sqlQuery->raw('type = ?')->put($type); + return new SqlEqualsOperator('type', new SqlBinding($type)); } protected static function __filterTokenDateParser($val) @@ -180,142 +213,101 @@ class PostSearchService extends AbstractSearchService return [$timeMin, $timeMax]; } - protected static function filterTokenDate($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenDate($val) { list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - $sqlQuery - ->raw('upload_date >= ?')->put($timeMin) - ->and('upload_date <= ?')->put($timeMax); + return (new SqlConjunction) + ->add(new SqlEqualsOrGreaterOperator('upload_date', new SqlBinding($timeMin))) + ->add(new SqlEqualsOrLesserOperator('upload_date', new SqlBinding($timeMax))); } - protected static function filterTokenDateMin($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenDateMin($val) { list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - $sqlQuery->raw('upload_date >= ?')->put($timeMin); + return new SqlEqualsOrGreaterOperator('upload_date', new SqlBinding($timeMin)); } - protected static function filterTokenDateMax($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenDateMax($val) { list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - $sqlQuery->raw('upload_date <= ?')->put($timeMax); + return new SqlEqualsOrLesserOperator('upload_date', new SqlBinding($timeMax)); } - protected static function filterTokenFav($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenFav($val) { $user = UserModel::findByNameOrEmail($val); - $sqlQuery - ->exists() - ->open() - ->select('1') - ->from('favoritee') - ->where('post_id = post.id') - ->and('favoritee.user_id = ?')->put($user->id) - ->close(); + $innerStmt = (new SqlSelectStatement) + ->setTable('favoritee') + ->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('favoritee.user_id', new SqlBinding($user->id)))); + return new SqlExistsOperator($innerStmt); } - protected static function filterTokenFavs($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenFavs($val) { - return self::filterTokenFav($searchContext, $sqlQuery, $val); + return self::filterTokenFav($val); } - protected static function filterTokenComment($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenComment($val) { $user = UserModel::findByNameOrEmail($val); - $sqlQuery - ->exists() - ->open() - ->select('1') - ->from('comment') - ->where('post_id = post.id') - ->and('commenter_id = ?')->put($user->id) - ->close(); + $innerStmt = (new SqlSelectStatement) + ->setTable('comment') + ->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('commenter_id', new SqlBinding($user->id)))); + return new SqlExistsOperator($innerStmt); } - protected static function filterTokenCommenter($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenCommenter($val) { - return self::filterTokenComment($searchContext, $sqlQuery, $val); + return self::filterTokenComment($searchContext, $stmt, $val); } - protected static function filterTokenSubmit($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenSubmit($val) { $user = UserModel::findByNameOrEmail($val); - $sqlQuery->raw('uploader_id = ?')->put($user->id); + return new SqlEqualsOperator('uploader_id', new SqlBinding($user->id)); } - protected static function filterTokenUploader($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenUploader($val) { - return self::filterTokenSubmit($searchContext, $sqlQuery, $val); + return self::filterTokenSubmit($val); } - protected static function filterTokenUpload($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenUpload($val) { - return self::filterTokenSubmit($searchContext, $sqlQuery, $val); + return self::filterTokenSubmit($val); } - protected static function filterTokenUploaded($searchContext, SqlQuery $sqlQuery, $val) + protected static function filterTokenUploaded($val) { - return self::filterTokenSubmit($searchContext, $sqlQuery, $val); - } - - protected static function filterTokenPrev($searchContext, SqlQuery $sqlQuery, $val) - { - self::__filterTokenPrevNext($searchContext, $sqlQuery, $val); - } - - protected static function filterTokenNext($searchContext, SqlQuery $sqlQuery, $val) - { - $searchContext->orderDir *= -1; - self::__filterTokenPrevNext($searchContext, $sqlQuery, $val); - } - - protected static function __filterTokenPrevNext($searchContext, SqlQuery $sqlQuery, $val) - { - $op1 = $searchContext->orderDir == 1 ? '<' : '>'; - $op2 = $searchContext->orderDir != 1 ? '<' : '>'; - $sqlQuery - ->open() - ->open() - ->raw($searchContext->orderColumn . ' ' . $op1 . ' ') - ->open() - ->select($searchContext->orderColumn) - ->from('post p2') - ->where('p2.id = ?')->put(intval($val)) - ->close() - ->and('id != ?')->put($val) - ->close() - ->or() - ->open() - ->raw($searchContext->orderColumn . ' = ') - ->open() - ->select($searchContext->orderColumn) - ->from('post p2') - ->where('p2.id = ?')->put(intval($val)) - ->close() - ->and('id ' . $op1 . ' ?')->put(intval($val)) - ->close() - ->close(); + return self::filterTokenSubmit($val); } - protected static function parseOrderToken($searchContext, $val) + + protected static function changeOrder($stmt, $val, $neg = true) { $randomReset = true; - $orderDir = 1; + $orderDir = SqlSelectStatement::ORDER_DESC; if (substr($val, -4) == 'desc') { - $orderDir = 1; + $orderDir = SqlSelectStatement::ORDER_DESC; $val = rtrim(substr($val, 0, -4), ','); } elseif (substr($val, -3) == 'asc') { - $orderDir = -1; + $orderDir = SqlSelectStatement::ORDER_ASC; $val = rtrim(substr($val, 0, -3), ','); } - if ($val{0} == '-') + if ($neg) { - $orderDir *= -1; - $val = substr($val, 1); + $orderDir = $orderDir == SqlSelectStatement::ORDER_DESC + ? SqlSelectStatement::ORDER_ASC + : SqlSelectStatement::ORDER_DESC; } switch ($val) @@ -329,11 +321,13 @@ class PostSearchService extends AbstractSearchService case 'comment': case 'comments': case 'commentcount': + case 'comment_count': $orderColumn = 'comment_count'; break; case 'fav': case 'favs': case 'favcount': + case 'fav_count': $orderColumn = 'fav_count'; break; case 'score': @@ -342,6 +336,7 @@ class PostSearchService extends AbstractSearchService case 'tag': case 'tags': case 'tagcount': + case 'tag_count': $orderColumn = 'tag_count'; break; case 'random': @@ -363,61 +358,26 @@ class PostSearchService extends AbstractSearchService if ($randomReset and isset($_SESSION['browsing-seed'])) unset($_SESSION['browsing-seed']); - $searchContext->orderColumn = $orderColumn; - $searchContext->orderDir = $orderDir; + $stmt->setOrderBy($orderColumn, $orderDir); } - protected static function iterateTokens($tokens, $callback) - { - $unparsedTokens = []; - - foreach ($tokens as $origToken) - { - $token = $origToken; - $neg = false; - if ($token{0} == '-') - { - $token = substr($token, 1); - $neg = true; - } - - $pos = strpos($token, ':'); - if ($pos === false) - { - $key = null; - $val = $token; - } - else - { - $key = substr($token, 0, $pos); - $val = substr($token, $pos + 1); - } - - $parsed = $callback($neg, $key, $val); - - if (!$parsed) - $unparsedTokens []= $origToken; - } - return $unparsedTokens; - } - - public static function decorate(SqlQuery $sqlQuery, $searchQuery) + public static function decorate(SqlSelectStatement $stmt, $searchQuery) { $config = \Chibi\Registry::getConfig(); - $sqlQuery->from('post'); + $stmt->setTable('post'); + $stmt->setCriterion(new SqlConjunction()); - self::filterChain($sqlQuery); - self::filterUserSafety($sqlQuery); + self::filterUserSafety($stmt); /* query tokens */ - $tokens = array_filter(array_unique(explode(' ', strtolower($searchQuery)))); + $tokens = array_filter(array_unique(preg_split('/\s+/', strtolower($searchQuery)))); if (self::$enableTokenLimit and count($tokens) > $config->browsing->maxSearchTokens) throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')'); - if (\Chibi\Registry::getContext()->user->hasEnabledHidingDislikedPosts()) + if (\Chibi\Registry::getContext()->user->hasEnabledHidingDislikedPosts() and !in_array('special:disliked', $tokens)) $tokens []= '-special:disliked'; if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden') or !in_array('special:hidden', $tokens)) $tokens []= '-special:hidden'; @@ -426,62 +386,43 @@ class PostSearchService extends AbstractSearchService $searchContext->orderColumn = 'id'; $searchContext->orderDir = 1; - $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery, &$orderToken) + foreach ($tokens as $token) { - if ($key != 'order') - return false; + $neg = false; + if ($token{0} == '-') + { + $neg = true; + $token = substr($token, 1); + } - if ($neg) - $orderToken = '-' . $val; + if (strpos($token, ':') !== false) + { + list ($key, $val) = explode(':', $token); + $key = strtolower($key); + if ($key == 'order') + { + self::changeOrder($stmt, $val, $neg); + } + else + { + $methodName = 'filterToken' . TextHelper::kebabCaseToCamelCase($key); + if (!method_exists(__CLASS__, $methodName)) + throw new SimpleException('Unknown search token "' . $key . '"'); + + $criterion = self::$methodName($val); + $criterion = self::decorateNegation($criterion, $neg); + $stmt->getCriterion()->add($criterion); + } + } else - $orderToken = $val; - self::parseOrderToken($searchContext, $orderToken); - - return true; - }); - - - $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery) - { - if ($key !== null) - return false; - - self::filterChain($sqlQuery); - if ($neg) - self::filterNegate($sqlQuery); - self::filterTag($sqlQuery, $val); - return true; - }); - - $tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery) - { - $methodName = 'filterToken' . TextHelper::kebabCaseToCamelCase($key); - if (!method_exists(__CLASS__, $methodName)) - return false; - - self::filterChain($sqlQuery); - if ($neg) - self::filterNegate($sqlQuery); - self::$methodName($searchContext, $sqlQuery, $val); - return true; - }); - - if (!empty($tokens)) - throw new SimpleException('Unknown search token "' . array_shift($tokens) . '"'); - - $sqlQuery->orderBy($searchContext->orderColumn); - if ($searchContext->orderDir == 1) - $sqlQuery->desc(); - else - $sqlQuery->asc(); - - if ($searchContext->orderColumn != 'id') - { - $sqlQuery->raw(', id'); - if ($searchContext->orderDir == 1) - $sqlQuery->desc(); - else - $sqlQuery->asc(); + { + self::filterTag($stmt, $token, $neg); + } } + + $stmt->addOrderBy('id', + empty($stmt->getOrderBy()) + ? SqlSelectStatement::ORDER_DESC + : $stmt->getOrderBy()[0][1]); } } diff --git a/src/Models/SearchServices/TagSearchService.php b/src/Models/SearchServices/TagSearchService.php index e162d536..4d60caf4 100644 --- a/src/Models/SearchServices/TagSearchService.php +++ b/src/Models/SearchServices/TagSearchService.php @@ -1,23 +1,15 @@ raw(', COUNT(post_tag.post_id)') - ->as('post_count') - ->from('tag') - ->innerJoin('post_tag') - ->on('tag.id = post_tag.tag_id') - ->innerJoin('post') - ->on('post.id = post_tag.post_id'); - if (empty($allowedSafety)) - $sqlQuery->where('0'); - else - $sqlQuery->where('safety')->in()->genSlots($allowedSafety); - foreach ($allowedSafety as $s) - $sqlQuery->put($s); + $stmt + ->addColumn('COUNT(post_tag.post_id) AS post_count') + ->setTable('tag') + ->addInnerJoin('post_tag', new SqlEqualsOperator('tag.id', 'post_tag.tag_id')) + ->addInnerJoin('post', new SqlEqualsOperator('post.id', 'post_tag.post_id')); + $stmt->setCriterion((new SqlConjunction)->add(SqlInOperator::fromArray('safety', SqlBinding::fromArray($allowedSafety)))); $orderToken = null; @@ -40,21 +32,17 @@ class TagSearchService extends AbstractSearchService if (strlen($token) >= 3) $token = '%' . $token; $token .= '%'; - $sqlQuery - ->and('tag.name') - ->like('?') - ->put($token) - ->collate()->nocase(); + $stmt->getCriterion()->add(new SqlNoCaseOperator(new SqlLikeOperator('tag.name', new SqlBinding($token)))); } } } - $sqlQuery->groupBy('tag.id'); + $stmt->groupBy('tag.id'); if ($orderToken) - self::order($sqlQuery,$orderToken); + self::order($stmt,$orderToken); } - private static function order(SqlQuery $sqlQuery, $value) + private static function order(SqlSelectStatement $stmt, $value) { if (strpos($value, ',') !== false) { @@ -69,17 +57,18 @@ class TagSearchService extends AbstractSearchService switch ($orderColumn) { case 'popularity': - $sqlQuery->orderBy('post_count'); + $stmt->setOrderBy('post_count', + $orderDir == 'asc' + ? SqlSelectStatement::ORDER_ASC + : SqlSelectStatement::ORDER_DESC); break; case 'alpha': - $sqlQuery->orderBy('tag.name')->collate()->nocase(); + $stmt->setOrderBy(new SqlNoCaseOperator('tag.name'), + $orderDir == 'asc' + ? SqlSelectStatement::ORDER_ASC + : SqlSelectStatement::ORDER_DESC); break; } - - if ($orderDir == 'asc') - $sqlQuery->asc(); - else - $sqlQuery->desc(); } } diff --git a/src/Models/SearchServices/UserSearchService.php b/src/Models/SearchServices/UserSearchService.php index 5df6d10f..0acb22db 100644 --- a/src/Models/SearchServices/UserSearchService.php +++ b/src/Models/SearchServices/UserSearchService.php @@ -1,28 +1,29 @@ from('user'); + $stmt->setTable('user'); $sortStyle = $searchQuery; switch ($sortStyle) { case 'alpha,asc': - $sqlQuery->orderBy('name')->collate()->nocase()->asc(); + $stmt->setOrderBy(new SqlNoCaseOperator('name'), SqlSelectStatement::ORDER_ASC); break; case 'alpha,desc': - $sqlQuery->orderBy('name')->collate()->nocase()->desc(); + $stmt->setOrderBy(new SqlNoCaseOperator('name'), SqlSelectStatement::ORDER_DESC); break; case 'date,asc': - $sqlQuery->orderBy('join_date')->asc(); + $stmt->setOrderBy('join_date', SqlSelectStatement::ORDER_ASC); break; case 'date,desc': - $sqlQuery->orderBy('join_date')->desc(); + $stmt->setOrderBy('join_date', SqlSelectStatement::ORDER_DESC); break; case 'pending': - $sqlQuery->where('staff_confirmed IS NULL'); - $sqlQuery->or('staff_confirmed = 0'); + $stmt->setCriterion((new SqlDisjunction) + ->add(new SqlIsNullOperator('staff_confirmed')) + ->add(new SqlEqualsOperator('staff_confirmed', '0'))); break; default: throw new SimpleException('Unknown sort style "' . $sortStyle . '"'); diff --git a/src/Models/TagModel.php b/src/Models/TagModel.php index 4131f5b2..e9972395 100644 --- a/src/Models/TagModel.php +++ b/src/Models/TagModel.php @@ -12,27 +12,29 @@ class TagModel extends AbstractCrudModel { self::forgeId($tag, 'tag'); - $query = (new SqlQuery) - ->update('tag') - ->set('name = ?')->put($tag->name) - ->where('id = ?')->put($tag->id); + $stmt = new SqlUpdateStatement(); + $stmt->setTable('tag'); + $stmt->setColumn('name', new SqlBinding($tag->name)); + $stmt->setCriterion(new SqlEqualsOperator('id', new SqlBinding($tag->id))); - Database::query($query); + Database::exec($stmt); }); return $tag->id; } public static function remove($tag) { - $query = (new SqlQuery) - ->deleteFrom('post_tag') - ->where('tag_id = ?')->put($tag->id); - Database::query($query); + $binding = new SqlBinding($tag->id); - $query = (new SqlQuery) - ->deleteFrom('tag') - ->where('id = ?')->put($tag->id); - Database::query($query); + $stmt = new SqlDeleteStatement(); + $stmt->setTable('post_tag'); + $stmt->setCriterion(new SqlEqualsOperator('tag_id', $binding)); + Database::exec($stmt); + + $stmt = new SqlDeleteStatement(); + $stmt->setTable('tag'); + $stmt->setCriterion(new SqlEqualsOperator('id', $binding)); + Database::exec($stmt); } public static function rename($sourceName, $targetName) @@ -60,38 +62,40 @@ class TagModel extends AbstractCrudModel if ($sourceTag->id == $targetTag->id) throw new SimpleException('Source and target tag are the same'); - $query = (new SqlQuery) - ->select('post.id') - ->from('post') - ->where() - ->exists() - ->open() - ->select('1') - ->from('post_tag') - ->where('post_tag.post_id = post.id') - ->and('post_tag.tag_id = ?')->put($sourceTag->id) - ->close() - ->and() - ->not()->exists() - ->open() - ->select('1') - ->from('post_tag') - ->where('post_tag.post_id = post.id') - ->and('post_tag.tag_id = ?')->put($targetTag->id) - ->close(); - $rows = Database::fetchAll($query); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('post.id'); + $stmt->setTable('post'); + $stmt->setCriterion( + (new SqlConjunction) + ->add( + new SqlExistsOperator( + (new SqlSelectStatement) + ->setTable('post_tag') + ->setCriterion( + (new SqlConjunction) + ->add(new SqlEqualsOperator('post_tag.post_id', 'post.id')) + ->add(new SqlEqualsOperator('post_tag.tag_id', new SqlBinding($sourceTag->id)))))) + ->add( + new SqlNegationOperator( + new SqlExistsOperator( + (new SqlSelectStatement) + ->setTable('post_tag') + ->setCriterion( + (new SqlConjunction) + ->add(new SqlEqualsOperator('post_tag.post_id', 'post.id')) + ->add(new SqlEqualsOperator('post_tag.tag_id', new SqlBinding($targetTag->id)))))))); + $rows = Database::fetchAll($stmt); $postIds = array_map(function($row) { return $row['id']; }, $rows); self::remove($sourceTag); foreach ($postIds as $postId) { - $query = (new SqlQuery) - ->insertInto('post_tag') - ->surround('post_id, tag_id') - ->values()->surround('?, ?') - ->put([$postId, $targetTag->id]); - Database::query($query); + $stmt = new SqlInsertStatement(); + $stmt->setTable('post_tag'); + $stmt->setColumn('post_id', new SqlBinding($postId)); + $stmt->setColumn('tag_id', new SqlBinding($targetTag->id)); + Database::exec($stmt); } }); } @@ -99,16 +103,13 @@ class TagModel extends AbstractCrudModel public static function findAllByPostId($key) { - $query = new SqlQuery(); - $query - ->select('tag.*') - ->from('tag') - ->innerJoin('post_tag') - ->on('post_tag.tag_id = tag.id') - ->where('post_tag.post_id = ?') - ->put($key); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('tag.*'); + $stmt->setTable('tag'); + $stmt->addInnerJoin('post_tag', new SqlEqualsOperator('post_tag.tag_id', 'tag.id')); + $stmt->setCriterion(new SqlEqualsOperator('post_tag.post_id', new SqlBinding($key))); - $rows = Database::fetchAll($query); + $rows = Database::fetchAll($stmt); if ($rows) return self::convertRows($rows); return []; @@ -116,13 +117,12 @@ class TagModel extends AbstractCrudModel public static function findByName($key, $throw = true) { - $query = (new SqlQuery) - ->select('*') - ->from('tag') - ->where('name = ?')->put($key) - ->collate()->nocase(); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('tag.*'); + $stmt->setTable('tag'); + $stmt->setCriterion(new SqlNoCaseOperator(new SqlEqualsOperator('name', new SqlBinding($key)))); - $row = Database::fetchOne($query); + $row = Database::fetchOne($stmt); if ($row) return self::convertRow($row); @@ -135,16 +135,15 @@ class TagModel extends AbstractCrudModel public static function removeUnused() { - $query = (new SqlQuery) - ->deleteFrom('tag') - ->where() - ->not()->exists() - ->open() - ->select('1') - ->from('post_tag') - ->where('post_tag.tag_id = tag.id') - ->close(); - Database::query($query); + $stmt = (new SqlDeleteStatement) + ->setTable('tag') + ->setCriterion( + new SqlNegationOperator( + new SqlExistsOperator( + (new SqlSelectStatement) + ->setTable('post_tag') + ->setCriterion(new SqlEqualsOperator('post_tag.tag_id', 'tag.id'))))); + Database::exec($stmt); } diff --git a/src/Models/TokenModel.php b/src/Models/TokenModel.php index c6c59770..26434576 100644 --- a/src/Models/TokenModel.php +++ b/src/Models/TokenModel.php @@ -20,12 +20,14 @@ implements IModel 'expires' => $token->expires, ]; - $query = (new SqlQuery) - ->update('user_token') - ->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) - ->put(array_values($bindings)) - ->where('id = ?')->put($token->id); - Database::query($query); + $stmt = new SqlUpdateStatement(); + $stmt->setTable('user_token'); + $stmt->setCriterion(new SqlEqualsOperator('id', new SqlBinding($token->id))); + + foreach ($bindings as $key => $val) + $stmt->setColumn($key, new SqlBinding($val)); + + Database::exec($stmt); }); } @@ -37,12 +39,12 @@ implements IModel if (empty($key)) throw new SimpleNotFoundException('Invalid security token'); - $query = (new SqlQuery) - ->select('*') - ->from('user_token') - ->where('token = ?')->put($key); + $stmt = new SqlSelectStatement(); + $stmt->setTable('user_token'); + $stmt->setColumn('*'); + $stmt->setCriterion(new SqlEqualsOperator('token', new SqlBinding($key))); - $row = Database::fetchOne($query); + $row = Database::fetchOne($stmt); if ($row) return self::convertRow($row); diff --git a/src/Models/UserModel.php b/src/Models/UserModel.php index 9855ef72..31e5ad46 100644 --- a/src/Models/UserModel.php +++ b/src/Models/UserModel.php @@ -40,12 +40,14 @@ class UserModel extends AbstractCrudModel 'banned' => $user->banned ]; - $query = (new SqlQuery) - ->update('user') - ->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) - ->put(array_values($bindings)) - ->where('id = ?')->put($user->id); - Database::query($query); + $stmt = (new SqlUpdateStatement) + ->setTable('user') + ->setCriterion(new SqlEqualsOperator('id', new SqlBinding($user->id))); + + foreach ($bindings as $key => $val) + $stmt->setColumn($key, new SqlBinding($val)); + + Database::exec($stmt); }); } @@ -53,32 +55,31 @@ class UserModel extends AbstractCrudModel { Database::transaction(function() use ($user) { - $queries = []; + $binding = new SqlBinding($user->id); - $queries []= (new SqlQuery) - ->deleteFrom('post_score') - ->where('user_id = ?')->put($user->id); + $stmt = new SqlDeleteStatement(); + $stmt->setTable('post_score'); + $stmt->setCriterion(new SqlEqualsOperator('user_id', $binding)); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->update('comment') - ->set('commenter_id = NULL') - ->where('commenter_id = ?')->put($user->id); + $stmt->setTable('favoritee'); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->update('post') - ->set('uploader_id = NULL') - ->where('uploader_id = ?')->put($user->id); + $stmt->setTable('user'); + $stmt->setCriterion(new SqlEqualsOperator('id', $binding)); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->deleteFrom('favoritee') - ->where('user_id = ?')->put($user->id); + $stmt = new SqlUpdateStatement(); + $stmt->setTable('comment'); + $stmt->setCriterion(new SqlEqualsOperator('commenter_id', $binding)); + $stmt->setColumn('commenter_id', new SqlNullOperator()); + Database::exec($stmt); - $queries []= (new SqlQuery) - ->deleteFrom('user') - ->where('id = ?')->put($user->id); - - foreach ($queries as $query) - Database::query($query); + $stmt = new SqlUpdateStatement(); + $stmt->setTable('post'); + $stmt->setCriterion(new SqlEqualsOperator('uploader_id', $binding)); + $stmt->setColumn('uploader_id', new SqlNullOperator()); + Database::exec($stmt); }); } @@ -86,13 +87,12 @@ class UserModel extends AbstractCrudModel public static function findByName($key, $throw = true) { - $query = (new SqlQuery) - ->select('*') - ->from('user') - ->where('name = ?')->put(trim($key)) - ->collate()->nocase(); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('*'); + $stmt->setTable('user'); + $stmt->setCriterion(new SqlNoCaseOperator(new SqlEqualsOperator('name', new SqlBinding(trim($key))))); - $row = Database::fetchOne($query); + $row = Database::fetchOne($stmt); if ($row) return self::convertRow($row); @@ -103,15 +103,14 @@ class UserModel extends AbstractCrudModel public static function findByNameOrEmail($key, $throw = true) { - $query = new SqlQuery(); - $query->select('*') - ->from('user') - ->where('name = ?')->put(trim($key)) - ->collate()->nocase() - ->or('email_confirmed = ?')->put(trim($key)) - ->collate()->nocase(); + $stmt = new SqlSelectStatement(); + $stmt->setColumn('*'); + $stmt->setTable('user'); + $stmt->setCriterion((new SqlDisjunction) + ->add(new SqlNoCaseOperator(new SqlEqualsOperator('name', new SqlBinding(trim($key))))) + ->add(new SqlNoCaseOperator(new SqlEqualsOperator('email_confirmed', new SqlBinding(trim($key)))))); - $row = Database::fetchOne($query); + $row = Database::fetchOne($stmt); if ($row) return self::convertRow($row); @@ -126,20 +125,21 @@ class UserModel extends AbstractCrudModel { Database::transaction(function() use ($user, $post, $score) { - $query = (new SqlQuery) - ->deleteFrom('post_score') - ->where('post_id = ?')->put($post->id) - ->and('user_id = ?')->put($user->id); - Database::query($query); + $stmt = new SqlDeleteStatement(); + $stmt->setTable('post_score'); + $stmt->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('post_id', new SqlBinding($post->id))) + ->add(new SqlEqualsOperator('user_id', new SqlBinding($user->id)))); + Database::exec($stmt); $score = intval($score); if ($score != 0) { - $query = (new SqlQuery); - $query->insertInto('post_score') - ->surround('post_id, user_id, score') - ->values()->surround('?, ?, ?') - ->put([$post->id, $user->id, $score]); - Database::query($query); + $stmt = new SqlInsertStatement(); + $stmt->setTable('post_score'); + $stmt->setColumn('post_id', new SqlBinding($post->id)); + $stmt->setColumn('user_id', new SqlBinding($user->id)); + $stmt->setColumn('score', new SqlBinding($score)); + Database::exec($stmt); } }); } @@ -149,12 +149,11 @@ class UserModel extends AbstractCrudModel Database::transaction(function() use ($user, $post) { self::removeFromUserFavorites($user, $post); - $query = (new SqlQuery); - $query->insertInto('favoritee') - ->surround('post_id, user_id') - ->values()->surround('?, ?') - ->put([$post->id, $user->id]); - Database::query($query); + $stmt = new SqlInsertStatement(); + $stmt->setTable('favoritee'); + $stmt->setColumn('post_id', new SqlBinding($post->id)); + $stmt->setColumn('user_id', new SqlBinding($user->id)); + Database::exec($stmt); }); } @@ -162,11 +161,12 @@ class UserModel extends AbstractCrudModel { Database::transaction(function() use ($user, $post) { - $query = (new SqlQuery) - ->deleteFrom('favoritee') - ->where('post_id = ?')->put($post->id) - ->and('user_id = ?')->put($user->id); - Database::query($query); + $stmt = new SqlDeleteStatement(); + $stmt->setTable('favoritee'); + $stmt->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('post_id', new SqlBinding($post->id))) + ->add(new SqlEqualsOperator('user_id', new SqlBinding($user->id)))); + Database::exec($stmt); }); } diff --git a/src/Sql/Operators/BinaryOperators/SqlAdditionOperator.php b/src/Sql/Operators/BinaryOperators/SqlAdditionOperator.php new file mode 100644 index 00000000..1aa52061 --- /dev/null +++ b/src/Sql/Operators/BinaryOperators/SqlAdditionOperator.php @@ -0,0 +1,8 @@ +subject->getAsString() . ') ' . $this->operator . ' ' . $this->target->getAsString(); + } +} diff --git a/src/Sql/Operators/BinaryOperators/SqlEqualsOperator.php b/src/Sql/Operators/BinaryOperators/SqlEqualsOperator.php new file mode 100644 index 00000000..19f3c3e0 --- /dev/null +++ b/src/Sql/Operators/BinaryOperators/SqlEqualsOperator.php @@ -0,0 +1,8 @@ +='); + } +} diff --git a/src/Sql/Operators/BinaryOperators/SqlEqualsOrLesserOperator.php b/src/Sql/Operators/BinaryOperators/SqlEqualsOrLesserOperator.php new file mode 100644 index 00000000..c98b6b03 --- /dev/null +++ b/src/Sql/Operators/BinaryOperators/SqlEqualsOrLesserOperator.php @@ -0,0 +1,8 @@ +'); + } +} diff --git a/src/Sql/Operators/BinaryOperators/SqlLesserOperator.php b/src/Sql/Operators/BinaryOperators/SqlLesserOperator.php new file mode 100644 index 00000000..eda21662 --- /dev/null +++ b/src/Sql/Operators/BinaryOperators/SqlLesserOperator.php @@ -0,0 +1,8 @@ +main->dbDriver == 'sqlite' + ? 'RANDOM()' + : 'RAND()'; + } +} diff --git a/src/Sql/Operators/SqlBinaryOperator.php b/src/Sql/Operators/SqlBinaryOperator.php new file mode 100644 index 00000000..ae411392 --- /dev/null +++ b/src/Sql/Operators/SqlBinaryOperator.php @@ -0,0 +1,19 @@ +subject = $this->attachExpression($subject); + $this->target = $this->attachExpression($target); + $this->operator = $operator; + } + + public function getAsString() + { + return '(' . $this->subject->getAsString() . ') ' . $this->operator . ' (' . $this->target->getAsString() . ')'; + } +} diff --git a/src/Sql/Operators/SqlNullaryOperator.php b/src/Sql/Operators/SqlNullaryOperator.php new file mode 100644 index 00000000..5b770acc --- /dev/null +++ b/src/Sql/Operators/SqlNullaryOperator.php @@ -0,0 +1,4 @@ +subject = $this->attachExpression($subject); + } + + public function getAsString() + { + if (empty($this->subject->getAsString())) + return $this->getAsStringEmpty(); + + return $this->getAsStringNonEmpty(); + } + + public function getAsStringEmpty() + { + return ''; + } + + public abstract function getAsStringNonEmpty(); +} diff --git a/src/Sql/Operators/SqlVariableOperator.php b/src/Sql/Operators/SqlVariableOperator.php new file mode 100644 index 00000000..330abc9a --- /dev/null +++ b/src/Sql/Operators/SqlVariableOperator.php @@ -0,0 +1,38 @@ +subjects = []; + } + + public function add($subject) + { + $this->subjects []= $this->attachExpression($subject); + return $this; + } + + public abstract function getAsStringNonEmpty(); + public abstract function getAsStringEmpty(); + + public function getAsString() + { + if (empty(array_filter($this->subjects, function($x) { return !empty($x->getAsString()); }))) + return $this->getAsStringEmpty(); + + return $this->getAsStringNonEmpty(); + } + + //variable arguments + public static function fromArray() + { + $args = func_get_args(); + $subjects = array_pop($args); + $instance = (new ReflectionClass(get_called_class()))->newInstanceArgs($args); + foreach ($subjects as $subject) + $instance->add($subject); + return $instance; + } +} diff --git a/src/Sql/Operators/UnaryOperators/SqlAbsOperator.php b/src/Sql/Operators/UnaryOperators/SqlAbsOperator.php new file mode 100644 index 00000000..09f60e34 --- /dev/null +++ b/src/Sql/Operators/UnaryOperators/SqlAbsOperator.php @@ -0,0 +1,8 @@ +subject->getAsString() . ')'; + } +} diff --git a/src/Sql/Operators/UnaryOperators/SqlCountOperator.php b/src/Sql/Operators/UnaryOperators/SqlCountOperator.php new file mode 100644 index 00000000..3a6ad805 --- /dev/null +++ b/src/Sql/Operators/UnaryOperators/SqlCountOperator.php @@ -0,0 +1,8 @@ +subject->getAsString() . ')'; + } +} diff --git a/src/Sql/Operators/UnaryOperators/SqlExistsOperator.php b/src/Sql/Operators/UnaryOperators/SqlExistsOperator.php new file mode 100644 index 00000000..e2e05739 --- /dev/null +++ b/src/Sql/Operators/UnaryOperators/SqlExistsOperator.php @@ -0,0 +1,8 @@ +subject->getAsString() . ')'; + } +} diff --git a/src/Sql/Operators/UnaryOperators/SqlIsNullOperator.php b/src/Sql/Operators/UnaryOperators/SqlIsNullOperator.php new file mode 100644 index 00000000..6576f1a0 --- /dev/null +++ b/src/Sql/Operators/UnaryOperators/SqlIsNullOperator.php @@ -0,0 +1,8 @@ +subject->getAsString() . ') IS NULL'; + } +} diff --git a/src/Sql/Operators/UnaryOperators/SqlNegationOperator.php b/src/Sql/Operators/UnaryOperators/SqlNegationOperator.php new file mode 100644 index 00000000..027bd800 --- /dev/null +++ b/src/Sql/Operators/UnaryOperators/SqlNegationOperator.php @@ -0,0 +1,8 @@ +subject->getAsString() . ')'; + } +} diff --git a/src/Sql/Operators/UnaryOperators/SqlNoCaseOperator.php b/src/Sql/Operators/UnaryOperators/SqlNoCaseOperator.php new file mode 100644 index 00000000..724a55f4 --- /dev/null +++ b/src/Sql/Operators/UnaryOperators/SqlNoCaseOperator.php @@ -0,0 +1,8 @@ +subject->getAsString() . ' COLLATE NOCASE'; + } +} diff --git a/src/Sql/Operators/VariableOperators/SqlConjunction.php b/src/Sql/Operators/VariableOperators/SqlConjunction.php new file mode 100644 index 00000000..6d69578b --- /dev/null +++ b/src/Sql/Operators/VariableOperators/SqlConjunction.php @@ -0,0 +1,16 @@ +getAsString(); + }, $this->subjects)) . ')'; + } +} diff --git a/src/Sql/Operators/VariableOperators/SqlDisjunction.php b/src/Sql/Operators/VariableOperators/SqlDisjunction.php new file mode 100644 index 00000000..e7c519b2 --- /dev/null +++ b/src/Sql/Operators/VariableOperators/SqlDisjunction.php @@ -0,0 +1,16 @@ +getAsString(); + }, $this->subjects)) . ')'; + } +} diff --git a/src/Sql/Operators/VariableOperators/SqlInOperator.php b/src/Sql/Operators/VariableOperators/SqlInOperator.php new file mode 100644 index 00000000..95c576f6 --- /dev/null +++ b/src/Sql/Operators/VariableOperators/SqlInOperator.php @@ -0,0 +1,23 @@ +subject = $this->attachExpression($subject); + } + + public function getAsStringEmpty() + { + return '0'; + } + + public function getAsStringNonEmpty() + { + return '(' . $this->subject->getAsString() . ') IN (' . join(', ', array_map(function($subject) + { + return $subject->getAsString(); + }, $this->subjects)) . ')'; + } +} diff --git a/src/Sql/SqlBinding.php b/src/Sql/SqlBinding.php new file mode 100644 index 00000000..a284b21c --- /dev/null +++ b/src/Sql/SqlBinding.php @@ -0,0 +1,31 @@ +content = $content; + $this->name = ':p' . (self::$bindingCount ++); + } + + public function getName() + { + return $this->name; + } + + public function getValue() + { + return $this->content; + } + + public static function fromArray(array $contents) + { + return array_map(function($content) + { + return new SqlBinding($content); + }, $contents); + } +} diff --git a/src/Sql/SqlExpression.php b/src/Sql/SqlExpression.php new file mode 100644 index 00000000..13d1f9e4 --- /dev/null +++ b/src/Sql/SqlExpression.php @@ -0,0 +1,45 @@ +bindings[$key] = $val; + return $this; + } + + public function getBindings() + { + $stack = array_merge([], $this->subExpressions); + $bindings = $this->bindings; + while (!empty($stack)) + { + $item = array_pop($stack); + $stack = array_merge($stack, $item->subExpressions); + $bindings = array_merge($bindings, $item->bindings); + } + return $bindings; + } + + public function attachExpression($object) + { + if ($object instanceof SqlBinding) + { + $this->bind($object->getName(), $object->getValue()); + return new SqlStringExpression($object->getName()); + } + else if ($object instanceof SqlExpression) + { + $this->subExpressions []= $object; + return $object; + } + else + { + return new SqlStringExpression((string) $object); + } + } +} diff --git a/src/Sql/SqlStringExpression.php b/src/Sql/SqlStringExpression.php new file mode 100644 index 00000000..977e15d3 --- /dev/null +++ b/src/Sql/SqlStringExpression.php @@ -0,0 +1,15 @@ +text = $text; + } + + public function getAsString() + { + return $this->text; + } +} diff --git a/src/Sql/Statements/SqlDeleteStatement.php b/src/Sql/Statements/SqlDeleteStatement.php new file mode 100644 index 00000000..d297ef87 --- /dev/null +++ b/src/Sql/Statements/SqlDeleteStatement.php @@ -0,0 +1,39 @@ +table; + } + + public function setTable($table) + { + $this->table = new SqlStringExpression($table); + return $this; + } + + public function getCriterion() + { + return $this->criterion; + } + + public function setCriterion($criterion) + { + $this->criterion = $this->attachExpression($criterion); + return $this; + } + + public function getAsString() + { + $sql = 'DELETE FROM ' . $this->table->getAsString() . ' '; + + if (!empty($this->criterion) and !empty($this->criterion->getAsString())) + $sql .= ' WHERE ' . $this->criterion->getAsString(); + + return $sql; + } +} + diff --git a/src/Sql/Statements/SqlInsertStatement.php b/src/Sql/Statements/SqlInsertStatement.php new file mode 100644 index 00000000..bc151869 --- /dev/null +++ b/src/Sql/Statements/SqlInsertStatement.php @@ -0,0 +1,62 @@ +table; + } + + public function setTable($table) + { + $this->table = new SqlStringExpression($table); + return $this; + } + + public function setColumn($column, $what) + { + $this->columns[$column] = $this->attachExpression($what); + $this->source = null; + return $this; + } + + public function setSource($columns, $what) + { + $this->source = $this->attachExpression($what); + $this->columns = $columns; + } + + public function getAsString() + { + $sql = 'INSERT INTO ' . $this->table->getAsString() . ' '; + if (!empty($this->source)) + { + $sql .= ' (' . join(', ', $this->columns) . ')'; + $sql .= ' ' . $this->source->getAsString(); + } + else + { + if (empty($this->columns)) + { + $config = \Chibi\Registry::getConfig(); + if ($config->main->dbDriver == 'sqlite') + $sql .= ' DEFAULT VALUES'; + else + $sql .= ' VALUES()'; + } + else + { + $sql .= ' (' . join(', ', array_keys($this->columns)) . ')'; + + $sql .= ' VALUES (' . join(', ', array_map(function($val) + { + return '(' . $val->getAsString() . ')'; + }, array_values($this->columns))) . ')'; + } + } + return $sql; + } +} diff --git a/src/Sql/Statements/SqlRawStatement.php b/src/Sql/Statements/SqlRawStatement.php new file mode 100644 index 00000000..cfea4250 --- /dev/null +++ b/src/Sql/Statements/SqlRawStatement.php @@ -0,0 +1,15 @@ +text = $text; + } + + public function getAsString() + { + return $this->text; + } +} diff --git a/src/Sql/Statements/SqlSelectStatement.php b/src/Sql/Statements/SqlSelectStatement.php new file mode 100644 index 00000000..1b1944f1 --- /dev/null +++ b/src/Sql/Statements/SqlSelectStatement.php @@ -0,0 +1,197 @@ +columns; + } + + public function resetColumns() + { + $this->columns = []; + return $this; + } + + public function setColumn($what) + { + $this->setColumns([$what]); + return $this; + } + + public function addColumn($what) + { + $this->columns []= $this->attachExpression($what); + return $this; + } + + public function setColumns($what) + { + $this->resetColumns(); + foreach ($what as $item) + $this->addColumn($item); + return $this; + } + + public function getTable() + { + return $this->source; + } + + public function setTable($table) + { + $this->source = new SqlStringExpression($table); + return $this; + } + + public function setSource(SqlExpression $source) + { + $this->source = $this->attachExpression($source); + return $this; + } + + public function addInnerJoin($table, SqlExpression $expression) + { + $this->innerJoins []= [$table, $this->attachExpression($expression)]; + return $this; + } + + public function addOuterJoin($table, $expression) + { + $this->innerJoins []= [$table, $this->attachExpression($expression)]; + return $this; + } + + public function getCriterion() + { + return $this->criterion; + } + + public function setCriterion($criterion) + { + $this->criterion = $this->attachExpression($criterion); + return $this; + } + + public function resetOrderBy() + { + $this->orderBy = []; + return $this; + } + + public function setOrderBy($what, $dir = self::ORDER_ASC) + { + $this->resetOrderBy(); + $this->addOrderBy($this->attachExpression($what), $dir); + return $this; + } + + public function addOrderBy($what, $dir = self::ORDER_ASC) + { + $this->orderBy []= [$this->attachExpression($what), $dir]; + return $this; + } + + public function getOrderBy() + { + return $this->orderBy; + } + + public function resetLimit() + { + $this->limit = null; + $this->offset = null; + return $this; + } + + public function setLimit($limit, $offset = null) + { + $this->limit = $this->attachExpression($limit); + $this->offset = $this->attachExpression($offset); + return $this; + } + + public function groupBy($groupBy) + { + $this->groupBy = $this->attachExpression($groupBy); + } + + public function getAsString() + { + $sql = 'SELECT '; + if (!empty($this->columns)) + $sql .= join(', ', array_map(function($column) + { + return $column->getAsString(); + }, $this->columns)); + else + $sql .= '1'; + $sql .= ' FROM (' . $this->source->getAsString() . ')'; + + foreach ($this->innerJoins as $join) + { + list ($table, $criterion) = $join; + $sql .= ' INNER JOIN ' . $table . ' ON ' . $criterion->getAsString(); + } + + foreach ($this->outerJoins as $outerJoin) + { + list ($table, $criterion) = $join; + $sql .= ' OUTER JOIN ' . $table . ' ON ' . $criterion->getAsString(); + } + + if (!empty($this->criterion) and !empty($this->criterion->getAsString())) + $sql .= ' WHERE ' . $this->criterion->getAsString(); + + if (!empty($this->groupBy) and !empty($this->groupBy->getAsString())) + { + $sql .= ' GROUP BY ' . $this->groupBy->getAsString(); + } + + if (!empty($this->orderBy)) + { + $f = true; + foreach ($this->orderBy as $orderBy) + { + $sql .= $f ? ' ORDER BY' : ', '; + $f = false; + list ($orderColumn, $orderDir) = $orderBy; + $sql .= ' ' . $orderColumn->getAsString(); + switch ($orderDir) + { + case self::ORDER_DESC: + $sql .= ' DESC'; + break; + case self::ORDER_ASC: + $sql .= ' ASC'; + break; + } + } + } + + if (!empty($this->limit) and !empty($this->limit->getAsString())) + { + $sql .= ' LIMIT '; + $sql .= $this->limit->getAsString(); + if (!empty($this->offset)) + { + $sql .= ' OFFSET '; + $sql .= $this->offset->getAsString(); + } + } + + return $sql; + } +} diff --git a/src/Sql/Statements/SqlStatement.php b/src/Sql/Statements/SqlStatement.php new file mode 100644 index 00000000..9b047d90 --- /dev/null +++ b/src/Sql/Statements/SqlStatement.php @@ -0,0 +1,4 @@ +table; + } + + public function setTable($table) + { + $this->table = new SqlStringExpression($table); + return $this; + } + + public function getCriterion() + { + return $this->criterion; + } + + public function setCriterion($criterion) + { + $this->criterion = $this->attachExpression($criterion); + return $this; + } + + public function setColumn($column, $what) + { + $this->columns[$column] = $this->attachExpression($what); + return $this; + } + + public function getAsString() + { + $sql = 'UPDATE ' . $this->table->getAsString(); + + if (!empty($this->columns)) + { + $sql .= ' SET ' . join(', ', array_map(function($key) + { + return $key . ' = (' . $this->columns[$key]->getAsString() . ')'; + }, array_keys($this->columns))); + } + + if (!empty($this->criterion) and !empty($this->criterion->getAsString())) + $sql .= ' WHERE ' . $this->criterion->getAsString(); + + return $sql; + } +} diff --git a/src/SqlQuery.php b/src/SqlQuery.php deleted file mode 100644 index 0bb6a3cf..00000000 --- a/src/SqlQuery.php +++ /dev/null @@ -1,99 +0,0 @@ -sql = ''; - $this->bindings = []; - } - - public function __call($name, array $arguments) - { - $name = TextHelper::camelCaseToKebabCase($name); - $name = str_replace('-', ' ', $name); - $this->sql .= $name . ' '; - - if (!empty($arguments)) - { - $arg = array_shift($arguments); - assert(empty($arguments)); - - if (is_object($arg)) - { - throw new Exception('Not implemented'); - } - else - { - $this->sql .= $arg . ' '; - } - } - - return $this; - } - - public function put($arg) - { - if (is_array($arg)) - { - foreach ($arg as $key => $val) - { - if (is_numeric($key)) - $this->bindings []= $val; - else - $this->bindings[$key] = $val; - } - } - else - { - $this->bindings []= $arg; - } - return $this; - } - - public function raw($raw) - { - $this->sql .= $raw . ' '; - return $this; - } - - public function open() - { - $this->sql .= '('; - return $this; - } - - public function close() - { - $this->sql .= ') '; - return $this; - } - - public function surround($raw) - { - $this->sql .= '(' . $raw . ') '; - return $this; - } - - public function genSlots($bindings) - { - if (empty($bindings)) - return $this; - $this->sql .= '('; - $this->sql .= join(',', array_fill(0, count($bindings), '?')); - $this->sql .= ') '; - return $this; - } - - public function getBindings() - { - return $this->bindings; - } - - public function getSql() - { - return trim($this->sql); - } -} diff --git a/src/Views/layout-normal.phtml b/src/Views/layout-normal.phtml index 45ef8383..d4b04f9d 100644 --- a/src/Views/layout-normal.phtml +++ b/src/Views/layout-normal.phtml @@ -50,7 +50,7 @@ LayoutHelper::addScript('core.js'); $bindings = []; foreach ($query->getBindings() as $k => $v) $bindings []= $k . '=' . $v; - printf('

%s [%s]

', htmlspecialchars($query->getSql()), join(', ', $bindings)); + printf('

%s [%s]

', htmlspecialchars($query->getAsString()), join(', ', $bindings)); } ?> diff --git a/upgrade.php b/upgrade.php index 595e8c14..18460887 100644 --- a/upgrade.php +++ b/upgrade.php @@ -4,7 +4,14 @@ $config = \Chibi\Registry::getConfig(); function getDbVersion() { - $dbVersion = PropertyModel::get(PropertyModel::DbVersion); + try + { + $dbVersion = PropertyModel::get(PropertyModel::DbVersion); + } + catch (Exception $e) + { + return [null, null]; + } if (strpos($dbVersion, '.') !== false) { list ($dbVersionMajor, $dbVersionMinor) = explode('.', $dbVersion); @@ -50,7 +57,7 @@ foreach ($upgrades as $upgradePath) { try { - Database::query((new SqlQuery)->raw($query)); + Database::exec(new SqlRawStatement($query)); } catch (Exception $e) {