From c6c0bc45a375a4221796a922a53ac2db95575b95 Mon Sep 17 00:00:00 2001 From: Brandon Fowler Date: Sat, 25 Nov 2023 02:36:58 -0500 Subject: [PATCH] Add CountQuery class --- includes/CountQuery.php | 148 ++++++++++++++++++++++++++++++++++++++++ includes/LinkCount.php | 134 ++++++++++-------------------------- 2 files changed, 183 insertions(+), 99 deletions(-) create mode 100644 includes/CountQuery.php diff --git a/includes/CountQuery.php b/includes/CountQuery.php new file mode 100644 index 0000000..1ec352c --- /dev/null +++ b/includes/CountQuery.php @@ -0,0 +1,148 @@ +fromNamespaces = $fromNamespaces; + $this->db = $db; + $this->title = $title; + } + + private function createCond( + $prefix, + $titleSQL, + $namespaceSQL, + $flags, + $joins = [], + $wheres = [] + ) { + $hasFromNS = ~$flags & CountQuery::NO_FROM_NS; + $usesLinkTarget = ~$flags & CountQuery::NO_LINK_TARGET; + $hasNS = $usesLinkTarget || (~$flags & CountQuery::SINGLE_NS); + + if ($this->fromNamespaces !== '' && $hasFromNS) { + array_push($wheres, "{$prefix}_from_namespace IN ({$this->fromNamespaces})"); + } + + if ($this->fromNamespaces !== '' && !$hasFromNS) { + array_push( + $joins, + <<fromNamespaces}) + SQL + ); + } + + $linkInfoPrefix = $usesLinkTarget ? 'lt' : $prefix; + $titleColumn = $linkInfoPrefix . '_' . ($hasNS ? 'title' : 'to'); + + array_push($wheres, "$titleColumn = $titleSQL"); + + if ($hasNS) { + array_push($wheres, "{$linkInfoPrefix}_namespace = $namespaceSQL"); + } + + if ($usesLinkTarget) { + array_push($joins, "JOIN linktarget ON {$prefix}_target_id = lt_id"); + } + + return implode(' ', $joins) . " WHERE " . implode(' AND ', $wheres); + } + + private function createDirectCond($prefix, $flags) { + return $this->createCond( + $prefix, + $this->db->quote($this->title->getDBKey()), + $this->title->getNamespaceId(), + $flags + ); + } + + private function createIndirectCond($table, $prefix, $flags) { + $joins = [ + 'JOIN page AS target ON target.page_id = rd_from', + "JOIN $table" + ]; + + $wheres = [ + "rd_title = {$this->db->quote($this->title->getDBKey())}", + "rd_namespace = {$this->title->getNamespaceId()}", + "(rd_interwiki IS NULL OR rd_interwiki = {$this->db->quote('')})" + ]; + + return $this->createCond( + $prefix, + 'target.page_title', + 'target.page_namespace', + $flags, + $joins, + $wheres + ); + } + + private function createQuery($table, $prefix, $mode, $flags) { + return match ($mode) { + self::MODE_REDIRECT => <<createDirectCond($prefix, $flags)} + AND ({$prefix}_interwiki is NULL or {$prefix}_interwiki = {$this->db->quote('')}) + SQL, + // Transclusions of a redirect that follow the redirect are also added as a transclusion of the redirect target. + // There is no way to differentiate from a page with a indirect link and a page with a indirect and a direct link + // in this case, only the indirect link is recorded. Pages can also transclude a page with a redirect without + // following the redirect, so a valid indirect link must have an associated direct link. + self::MODE_TRANSCLUSION => <<createIndirectCond($table, $prefix, $flags)} + ) AS temp ON {$prefix}_from = indirect_link + {$this->createDirectCond($prefix, $flags)} + SQL, + self::MODE_LINK => <<createDirectCond($prefix, $flags)} + UNION ALL + SELECT DISTINCT NULL AS direct_link, {$prefix}_from AS indirect_link + FROM redirect + {$this->createIndirectCond($table, $prefix, $flags)} + ) AS temp + SQL + }; + } + + public function runQuery($table, $prefix, $mode, $flags = 0) { + $query = $this->createQuery($table, $prefix, $mode, $flags); + $res = $this->db->query($query)->fetch(); + + return $mode == self::MODE_REDIRECT ? (int) $res[0] : [ + 'all' => (int) $res[0], + 'direct' => (int) $res[1], + 'indirect' => (int) $res[2] + ]; + } +} \ No newline at end of file diff --git a/includes/LinkCount.php b/includes/LinkCount.php index 5ea16b8..b66aca8 100644 --- a/includes/LinkCount.php +++ b/includes/LinkCount.php @@ -8,10 +8,9 @@ class LinkCount implements HtmlProducer, JsonProducer { public $counts; public $error; - private $fromNamespaces; private $projectURL; - private $db; private $title; + private $countQuery; private $typeInfo = [ 'filelinks' => [ @@ -57,8 +56,6 @@ public function __construct($page, $project, $namespaces = '') { } } - $this->fromNamespaces = $namespaces; - $maybeProjectURL = 'https://' . preg_replace('/^https:\/\//', '', $project); $metaDB = DatabaseFactory::create(); @@ -71,15 +68,43 @@ public function __construct($page, $project, $namespaces = '') { } list($dbName, $this->projectURL) = $stmt->fetch(); - $this->db = DatabaseFactory::create($dbName); + $db = DatabaseFactory::create($dbName); $this->title = new Title($page, $dbName, $this->projectURL); + $this->countQuery = new CountQuery($namespaces, $db, $this->title); $this->counts = [ - 'filelinks' => $this->title->getNamespaceId() === 6 ? $this->counts('imagelinks', 'il', self::COUNT_MODE_TRANSCLUSION, true, true, false) : null, - 'categorylinks' => $this->title->getNamespaceId() === 14 ? $this->counts('categorylinks', 'cl', self::COUNT_MODE_LINK, true, false, false) : null, - 'wikilinks' => $this->counts('pagelinks', 'pl', self::COUNT_MODE_LINK, false, true, false), - 'redirects' => $this->counts('redirect', 'rd', self::COUNT_MODE_REDIRECT, false, false, false), - 'transclusions' => $this->counts('templatelinks', 'tl', self::COUNT_MODE_TRANSCLUSION, false, true, true) + 'filelinks' => $this->title->getNamespaceId() === 6 + ? $this->countQuery->runQuery( + 'imagelinks', + 'il', + CountQuery::MODE_TRANSCLUSION, + CountQuery::SINGLE_NS | CountQuery::NO_LINK_TARGET + ) + : null, + 'categorylinks' => $this->title->getNamespaceId() === 14 + ? $this->countQuery->runQuery( + 'categorylinks', + 'cl', + CountQuery::MODE_LINK, + CountQuery::SINGLE_NS | CountQuery::NO_FROM_NS | CountQuery::NO_LINK_TARGET + ) + : null, + 'wikilinks' => $this->countQuery->runQuery( + 'pagelinks', + 'pl', + CountQuery::MODE_LINK, + CountQuery::NO_LINK_TARGET + ), + 'redirects' => $this->countQuery->runQuery( + 'redirect', + 'rd', + CountQuery::MODE_REDIRECT, + CountQuery::NO_FROM_NS | CountQuery::NO_LINK_TARGET), + 'transclusions' => $this->countQuery->runQuery( + 'templatelinks', + 'tl', + CountQuery::MODE_TRANSCLUSION + ) ]; // Redirects are included in the wikilinks table @@ -87,95 +112,6 @@ public function __construct($page, $project, $namespaces = '') { $this->counts['wikilinks']['direct'] -= $this->counts['redirects']; } - private function counts($table, $prefix, $mode = self::COUNT_MODE_LINK, $singleNS = false, $hasFromNamespace = true, $usesLinkTarget = true) { - $escapedTitle = $this->db->quote($this->title->getDBKey()); - $escapedBlank = $this->db->quote(''); - $titleColumn = $prefix . '_' . ($singleNS ? 'to' : 'title'); - - $fromNamespaceWhere = ''; - $fromNamespaceJoin = ''; - - if ($this->fromNamespaces !== '') { - $fromNamespaceWhere = $hasFromNamespace ? " AND {$prefix}_from_namespace IN ({$this->fromNamespaces})" : ''; - $fromNamespaceJoin = !$hasFromNamespace ? " JOIN page AS source ON source.page_id = {$prefix}_from AND source.page_namespace IN ({$this->fromNamespaces})" : ''; - } - - // TODO: Remove once all tables are switched to linktarget - $directCond = ''; - $indirectQuery = ''; - - if (!$usesLinkTarget) { - $namespaceComponent = $singleNS ? '' : " AND {$prefix}_namespace = {$this->title->getNamespaceId()}"; - - $directCond = <<title->getNamespaceId()} AND (rd_interwiki IS NULL OR rd_interwiki = $escapedBlank) - SQL; - } else { - $directCond = <<title->getNamespaceId()} $fromNamespaceWhere - SQL; - - $indirectQuery = <<title->getNamespaceId()} AND (rd_interwiki IS NULL OR rd_interwiki = $escapedBlank) - SQL; - } - - if ($mode == self::COUNT_MODE_REDIRECT) { - $query = <<db->query($query)->fetch(); - - return $mode == self::COUNT_MODE_REDIRECT ? (int) $res[0] : [ - 'all' => (int) $res[0], - 'direct' => (int) $res[1], - 'indirect' => (int) $res[2] - ]; - } - public function getTitle() { $parts = [];