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) {