Skip to content

Commit

Permalink
Title tree store #1
Browse files Browse the repository at this point in the history
  • Loading branch information
it-spiderman committed Dec 8, 2023
1 parent da10eb5 commit 855a63e
Show file tree
Hide file tree
Showing 15 changed files with 588 additions and 22 deletions.
2 changes: 1 addition & 1 deletion bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
return;
}

define( 'MWSTAKE_MEDIAWIKI_COMPONENT_COMMONWEBAPIS_VERSION', '2.0.5' );
define( 'MWSTAKE_MEDIAWIKI_COMPONENT_COMMONWEBAPIS_VERSION', '2.0.6' );

MWStake\MediaWiki\ComponentLoader\Bootstrapper::getInstance()
->register( 'commonwebapis', static function () {
Expand Down
13 changes: 13 additions & 0 deletions route-files/title-tree-store.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[
{
"path": "/mws/v1/title-tree-store",
"class": "MWStake\\MediaWiki\\Component\\CommonWebAPIs\\Rest\\TitleTreeStore",
"services": [
"HookContainer",
"DBLoadBalancer",
"TitleFactory",
"ContentLanguage",
"NamespaceInfo"
]
}
]
1 change: 1 addition & 0 deletions src/Data/TitleQueryStore/PrimaryDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ protected function appendRowToData( \stdClass $row ) {
TitleRecord::PAGE_DBKEY => $row->page_title,
TitleRecord::PAGE_CONTENT_MODEL => $row->page_content_model,
TitleRecord::IS_CONTENT_PAGE => in_array( $row->page_namespace, $this->contentNamespaces ),
TitleRecord::PAGE_EXISTS => true,
] );
}

Expand Down
27 changes: 19 additions & 8 deletions src/Data/TitleQueryStore/SecondaryDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,35 @@ public function __construct( $titleFactory, \Language $language ) {
public function extend( $dataSets ) {
foreach ( $dataSets as $dataSet ) {
$title = $this->titleFromRecord( $dataSet );

$dataSet->set( TitleRecord::PAGE_TITLE, $title->getText() );
$dataSet->set( TitleRecord::PAGE_PREFIXED, $title->getPrefixedText() );
$dataSet->set( TitleRecord::PAGE_URL, $title->getLocalURL() );
$dataSet->set(
TitleRecord::PAGE_NAMESPACE_TEXT, $this->language->getNsText( $title->getNamespace() )
);
$this->extendWithTitle( $dataSet, $title );
}

return $dataSets;
}

/**
* @param Record $dataSet
* @param \Title $title
*
* @return void
*/
protected function extendWithTitle( Record $dataSet, \Title $title ) {
$dataSet->set( TitleRecord::PAGE_TITLE, $title->getText() );
$dataSet->set( TitleRecord::PAGE_PREFIXED, $title->getPrefixedText() );
$dataSet->set( TitleRecord::PAGE_URL, $title->getLocalURL() );
$dataSet->set(
TitleRecord::PAGE_NAMESPACE_TEXT, $this->language->getNsText( $title->getNamespace() )
);
}

/**
* @param Record $record
*
* @return \Title|null
*/
protected function titleFromRecord( $record ) {
return $this->titleFactory->newFromID( $record->get( TitleRecord::PAGE_ID ) );
return $this->titleFactory->makeTitle(
$record->get( TitleRecord::PAGE_NAMESPACE ), $record->get( TitleRecord::PAGE_TITLE)
);
}
}
6 changes: 3 additions & 3 deletions src/Data/TitleQueryStore/TitleSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
use MWStake\MediaWiki\Component\DataStore\Schema;

class TitleSchema extends Schema {
public function __construct() {
parent::__construct( [
public function __construct( array $fields = [] ) {
parent::__construct( array_merge( [
TitleRecord::PAGE_ID => [
self::FILTERABLE => false,
self::SORTABLE => false,
Expand Down Expand Up @@ -58,6 +58,6 @@ public function __construct() {
self::SORTABLE => true ,
self::TYPE => FieldType::BOOLEAN
]
] );
], $fields ) );
}
}
256 changes: 256 additions & 0 deletions src/Data/TitleTreeStore/PrimaryDataProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<?php

namespace MWStake\MediaWiki\Component\CommonWebAPIs\Data\TitleTreeStore;

use MWStake\MediaWiki\Component\CommonWebAPIs\Data\TitleQueryStore\TitleRecord;
use MWStake\MediaWiki\Component\DataStore\Filter;
use MWStake\MediaWiki\Component\DataStore\ReaderParams;
use MWStake\MediaWiki\Component\DataStore\Schema;
use Wikimedia\Rdbms\IDatabase;

class PrimaryDataProvider extends \MWStake\MediaWiki\Component\CommonWebAPIs\Data\TitleQueryStore\PrimaryDataProvider {
/** @var string|null */
private $query = null;

/** @var \NamespaceInfo */
private $nsInfo;

/**
* @inheritDoc
*/
public function __construct( IDatabase $db, Schema $schema, \Language $language, \NamespaceInfo $nsInfo ) {
parent::__construct( $db, $schema, $language, $nsInfo );
$this->nsInfo = $nsInfo;
}

/**
* @param ReaderParams $params
*
* @return \MWStake\MediaWiki\Component\DataStore\Record[]
*/
public function makeData( $params ) {
if ( $params->getNode() !== '' ) {
$node = $params->getNode();
$node = $this->nodeToUniqueId( $node );
if ( $node ) {
return $this->dataFromNode( $node );
}
}
return array_values( parent::makeData( $params ) );
}

/**
* @param ReaderParams $params
*
* @return array
*/
protected function makePreFilterConds( ReaderParams $params ) {
if ( $params->getQuery() !== '' ) {
$this->query = mb_strtolower( str_replace( '_', ' ', $params->getQuery() ) );
}
return parent::makePreFilterConds( $params );
}

/**
* @param \stdClass $row
*
* @return void
*/
protected function appendRowToData( \stdClass $row ) {
$indexTitle = $row->mti_title;
$uniqueId = $this->getUniqueId( $row );
if ( $this->isSubpage( $indexTitle ) ) {
if ( $this->queryMatchesSubpage( $indexTitle ) ) {
$this->insertParents( $row, $uniqueId, true );
}
return;
}

// Adding root pages
$this->data[$uniqueId] = $this->makeRecord( $row, $uniqueId, false, false );
}

/**
* @param \stdClass $row
* @param string $uniqueId
* @param bool $expanded
* @param bool $loaded
*
* @return TitleTreeRecord
*/
private function makeRecord( $row, string $uniqueId, bool $expanded, bool $loaded ) {
return new TitleTreeRecord( (object)[
TitleTreeRecord::ID => $uniqueId,
TitleTreeRecord::PAGE_NAMESPACE => (int)$row->page_namespace,
TitleTreeRecord::PAGE_TITLE => $row->page_title,
TitleTreeRecord::PAGE_DBKEY => $row->page_title,
TitleTreeRecord::IS_CONTENT_PAGE => in_array( $row->page_namespace, $this->contentNamespaces ),
TitleTreeRecord::LEAF => false,
TitleTreeRecord::EXPANDED => $expanded,
TitleTreeRecord::LOADED => $loaded,
TitleTreeRecord::CHILDREN => property_exists( $row, 'children' ) ? $row->children : []
] );
}

/**
* @param $row
*
* @return string
*/
private function getUniqueId( $row ): string {
return (int)$row->page_namespace . ':' . $row->page_title;
}

/**
* @param string $indexTitle
*
* @return bool
*/
private function isSubpage( string $indexTitle ): bool {
return strpos( $indexTitle, '/' ) !== false;
}


/**
* @param string $indexTitle
*
* @return bool
*/
private function queryMatchesSubpage( string $indexTitle ): bool {
if ( !$this->query ) {
return true;
}
$exploded = explode( '/', $indexTitle );
// Get rid of the first element, which is the root page name
array_shift( $exploded );
return strpos( $indexTitle, $this->query ) !== false;
}

/**
* @param \stdClass $row
* @param string $uniqueId
* @param bool|null $fromQuery True if row comes from the query, not from traversing the tree
*
* @return void
*/
private function insertParents( \stdClass $row, string $uniqueId, ?bool $fromQuery = false ): void {
$title = $row->page_title;
$bits = explode( '/', $title );
if ( count( $bits ) === 1 ) {
$this->data[$uniqueId] = $this->makeRecord( $row, $uniqueId, !$fromQuery, !$fromQuery );
return;
}
array_pop( $bits );
$parentTitle = implode( '/', $bits );
$parentRow = new \stdClass();
$parentRow->page_title = $parentTitle;
$parentRow->page_namespace = $row->page_namespace;
$parentRow->children = $this->getChildren(
$parentRow,
$this->makeRecord( $row, $uniqueId, !$fromQuery, !$fromQuery, )
);
$this->insertParents( $parentRow, $this->getUniqueId( $parentRow ) );
}

/**
* @param \stdClass $row
* @param TitleTreeRecord|null $loadedChild
*
* @return TitleTreeRecord[]
*/
private function getChildren( \stdClass $row, ?TitleTreeRecord $loadedChild ): array {
$childRows = $this->getSubpages( $row );
$children = $loadedChild ? [ $loadedChild ] : [];
foreach ( $childRows as $childRow ) {
$uniqueChildId = $this->getUniqueId( $childRow );
if ( $loadedChild && $loadedChild->get( TitleTreeRecord::ID ) === $uniqueChildId ) {
continue;
}
if ( !$this->isDirectChildOf( $row->page_title, $childRow->page_title ) ) {
continue;
}
$child = $this->makeRecord( $childRow, $uniqueChildId, false, false );
$children[] = $child;
}

return $children;
}

/**
* @param \stdClass $row
*
* @return \Wikimedia\Rdbms\IResultWrapper
*/
private function getSubpages( \stdClass $row ) {
return $this->db->select(
[ 'page' ],
[ 'page_title', 'page_namespace' ],
[
'page_namespace' => $row->page_namespace,
'page_title LIKE ' . $this->db->addQuotes( $row->page_title . '/%' )
],
__METHOD__
);
}

/**
* @param $parent
* @param $child
*
* @return bool
*/
private function isDirectChildOf( $parent, $child ) {
$parentBits = explode( '/', $parent );
$childBits = explode( '/', $child );
if ( count( $childBits ) !== count( $parentBits ) + 1 ) {
return false;
}
for ( $i = 0; $i < count( $parentBits ); $i++ ) {
if ( $parentBits[$i] !== $childBits[$i] ) {
return false;
}
}
return true;
}

/**
* @param string $node
*
* @return array|null
*/
private function nodeToUniqueId( string $node ): ?array {
$bits = explode( ':', $node );
if ( count( $bits ) === 1 ) {
return '0:' . $bits[0];
}
$ns = $bits[0];
$nsIndex = $this->language->getNsIndex( $ns );
if ( $nsIndex === null ) {
return null;
}
return [
'page_namespace' => $nsIndex,
'page_title' => implode( ':', array_slice( $bits, 1 ) )
];
}

/**
* @param array $node
*
* @return array|TitleTreeRecord[]
*/
private function dataFromNode( array $node ): array {
$row = $this->db->selectRow(
[ 'page' ],
[ 'page_title', 'page_namespace' ],
$node,
__METHOD__
);

if ( !$row ) {
return [];
}

return $this->getChildren( $row, null );
}
}
42 changes: 42 additions & 0 deletions src/Data/TitleTreeStore/Reader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace MWStake\MediaWiki\Component\CommonWebAPIs\Data\TitleTreeStore;

use MWStake\MediaWiki\Component\DataStore\ReaderParams;
use Wikimedia\Rdbms\ILoadBalancer;

class Reader extends \MWStake\MediaWiki\Component\CommonWebAPIs\Data\TitleQueryStore\Reader {
/** @var ILoadBalancer */
protected $lb;
/** @var \TitleFactory */
protected $titleFactory;
/** @var \Language */
protected $language;
/** @var \NamespaceInfo */
protected $nsInfo;

/**
* @return UserSchema
*/
public function getSchema() {
return new TitleTreeSchema();
}

/**
* @param ReaderParams $params
*
* @return PrimaryDataProvider
*/
public function makePrimaryDataProvider( $params ) {
return new PrimaryDataProvider(
$this->lb->getConnection( DB_REPLICA ), $this->getSchema(), $this->language, $this->nsInfo
);
}

/**
* @return SecondaryDataProvider
*/
public function makeSecondaryDataProvider() {
return new SecondaryDataProvider( $this->titleFactory, $this->language );
}
}
Loading

0 comments on commit 855a63e

Please sign in to comment.