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
This commit is contained in:
Marcin Kurczewski 2014-02-22 19:21:32 +01:00
parent 1baceb5816
commit 6af3a0e42b
55 changed files with 1345 additions and 762 deletions

View file

@ -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;

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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 [];

View file

@ -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;

View file

@ -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'];
}
}

View file

@ -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'];
}
}

View file

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

View file

@ -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;
});

View file

@ -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'];
}
}

View file

@ -1,21 +1,14 @@
<?php
class CommentSearchService extends AbstractSearchService
{
public static function decorate(SqlQuery $sqlQuery, $searchQuery)
public static function decorate(SqlSelectStatement $stmt, $searchQuery)
{
$sqlQuery
->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);
}
}

View file

@ -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
$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;
throw new SimpleException('Unknown search token "' . $key . '"');
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();
$criterion = self::$methodName($val);
$criterion = self::decorateNegation($criterion, $neg);
$stmt->getCriterion()->add($criterion);
}
}
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]);
}
}

View file

@ -1,23 +1,15 @@
<?php
class TagSearchService extends AbstractSearchService
{
public static function decorate(SqlQuery $sqlQuery, $searchQuery)
public static function decorate(SqlSelectStatement $stmt, $searchQuery)
{
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$sqlQuery
->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();
}
}

View file

@ -1,28 +1,29 @@
<?php
class UserSearchService extends AbstractSearchService
{
protected static function decorate(SQLQuery $sqlQuery, $searchQuery)
protected static function decorate(SqlSelectStatement $stmt, $searchQuery)
{
$sqlQuery->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 . '"');

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
});
}

View file

@ -0,0 +1,8 @@
<?php
class SqlAdditionOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, '+');
}
}

View file

@ -0,0 +1,13 @@
<?php
class SqlAliasOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, 'AS');
}
public function getAsString()
{
return '(' . $this->subject->getAsString() . ') ' . $this->operator . ' ' . $this->target->getAsString();
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlEqualsOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, '=');
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlEqualsOrGreaterOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, '>=');
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlEqualsOrLesserOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, '<=');
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlGreaterOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, '>');
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlLesserOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, '<');
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlLikeOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, 'LIKE');
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlSubtractionOperator extends SqlBinaryOperator
{
public function __construct($subject, $target)
{
parent::__construct($subject, $target, '-');
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlNullOperator extends SqlNullaryOperator
{
public function getAsString()
{
return 'NULL';
}
}

View file

@ -0,0 +1,11 @@
<?php
class SqlRandomOperator extends SqlNullaryOperator
{
public function getAsString()
{
$config = \Chibi\Registry::getConfig();
return $config->main->dbDriver == 'sqlite'
? 'RANDOM()'
: 'RAND()';
}
}

View file

@ -0,0 +1,19 @@
<?php
class SqlBinaryOperator extends SqlOperator
{
protected $subject;
protected $target;
protected $operator;
public function __construct($subject, $target, $operator)
{
$this->subject = $this->attachExpression($subject);
$this->target = $this->attachExpression($target);
$this->operator = $operator;
}
public function getAsString()
{
return '(' . $this->subject->getAsString() . ') ' . $this->operator . ' (' . $this->target->getAsString() . ')';
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class SqlNullaryOperator extends SqlOperator
{
}

View file

@ -0,0 +1,4 @@
<?php
abstract class SqlOperator extends SqlExpression
{
}

View file

@ -0,0 +1,25 @@
<?php
abstract class SqlUnaryOperator extends SqlOperator
{
protected $subject;
public function __construct($subject)
{
$this->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();
}

View file

@ -0,0 +1,38 @@
<?php
abstract class SqlVariableOperator extends SqlOperator
{
protected $subjects;
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlAbsOperator extends SqlUnaryOperator
{
public function getAsStringNonEmpty()
{
return 'ABS (' . $this->subject->getAsString() . ')';
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlCountOperator extends SqlUnaryOperator
{
public function getAsStringNonEmpty()
{
return 'COUNT (' . $this->subject->getAsString() . ')';
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlExistsOperator extends SqlUnaryOperator
{
public function getAsStringNonEmpty()
{
return 'EXISTS (' . $this->subject->getAsString() . ')';
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlIsNullOperator extends SqlUnaryOperator
{
public function getAsStringNonEmpty()
{
return '(' . $this->subject->getAsString() . ') IS NULL';
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlNegationOperator extends SqlUnaryOperator
{
public function getAsStringNonEmpty()
{
return 'NOT (' . $this->subject->getAsString() . ')';
}
}

View file

@ -0,0 +1,8 @@
<?php
class SqlNoCaseOperator extends SqlUnaryOperator
{
public function getAsStringNonEmpty()
{
return $this->subject->getAsString() . ' COLLATE NOCASE';
}
}

View file

@ -0,0 +1,16 @@
<?php
class SqlConjunction extends SqlVariableOperator
{
public function getAsStringEmpty()
{
return '1';
}
public function getAsStringNonEmpty()
{
return '(' . join(' AND ', array_map(function($subject)
{
return $subject->getAsString();
}, $this->subjects)) . ')';
}
}

View file

@ -0,0 +1,16 @@
<?php
class SqlDisjunction extends SqlVariableOperator
{
public function getAsStringEmpty()
{
return '1';
}
public function getAsStringNonEmpty()
{
return '(' . join(' OR ', array_map(function($subject)
{
return $subject->getAsString();
}, $this->subjects)) . ')';
}
}

View file

@ -0,0 +1,23 @@
<?php
class SqlInOperator extends SqlVariableOperator
{
protected $subject;
public function __construct($subject)
{
$this->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)) . ')';
}
}

31
src/Sql/SqlBinding.php Normal file
View file

@ -0,0 +1,31 @@
<?php
class SqlBinding
{
protected $content;
protected $name;
private static $bindingCount = 0;
public function __construct($content)
{
$this->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);
}
}

45
src/Sql/SqlExpression.php Normal file
View file

@ -0,0 +1,45 @@
<?php
abstract class SqlExpression
{
abstract public function getAsString();
protected $bindings = [];
protected $subExpressions = [];
private function bind($key, $val)
{
$this->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);
}
}
}

View file

@ -0,0 +1,15 @@
<?php
class SqlStringExpression extends SqlExpression
{
protected $text;
public function __construct($text)
{
$this->text = $text;
}
public function getAsString()
{
return $this->text;
}
}

View file

@ -0,0 +1,39 @@
<?php
class SqlDeleteStatement extends SqlStatement
{
protected $table;
protected $criterion;
public function getTable()
{
return $this->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;
}
}

View file

@ -0,0 +1,62 @@
<?php
class SqlInsertStatement extends SqlStatement
{
protected $table;
protected $columns;
protected $source;
public function getTable()
{
return $this->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;
}
}

View file

@ -0,0 +1,15 @@
<?php
class SqlRawStatement extends SqlStatement
{
protected $text;
public function __construct($text)
{
$this->text = $text;
}
public function getAsString()
{
return $this->text;
}
}

View file

@ -0,0 +1,197 @@
<?php
class SqlSelectStatement extends SqlStatement
{
const ORDER_ASC = 1;
const ORDER_DESC = 2;
protected $columns = null;
protected $source = null;
protected $innerJoins = [];
protected $outerJoins = [];
protected $criterion = null;
protected $orderBy = [];
protected $limit = null;
protected $offset = null;
protected $groupBy = null;
public function getColumns()
{
return $this->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;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class SqlStatement extends SqlExpression
{
}

View file

@ -0,0 +1,53 @@
<?php
class SqlUpdateStatement extends SqlStatement
{
protected $table;
protected $criterion;
protected $columns;
public function getTable()
{
return $this->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;
}
}

View file

@ -1,99 +0,0 @@
<?php
class SqlQuery
{
protected $sql;
protected $bindings;
public function __construct()
{
$this->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);
}
}

View file

@ -50,7 +50,7 @@ LayoutHelper::addScript('core.js');
$bindings = [];
foreach ($query->getBindings() as $k => $v)
$bindings []= $k . '=' . $v;
printf('<p>%s [%s]</p>', htmlspecialchars($query->getSql()), join(', ', $bindings));
printf('<p>%s [%s]</p>', htmlspecialchars($query->getAsString()), join(', ', $bindings));
} ?>
</pre>
<?php endif ?>

View file

@ -3,8 +3,15 @@ require_once 'src/core.php';
$config = \Chibi\Registry::getConfig();
function getDbVersion()
{
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)
{