From 0bbba6e0d4a99a63c020acb402b6e57baa4863b0 Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Wed, 14 Sep 2016 03:11:41 -0700 Subject: [PATCH] Move various DB helper classes to /libs/rdbms Change-Id: I0724f1acce4f6c43b1f0983fa119e628e7c53ba5 --- autoload.php | 30 +- includes/db/DatabaseMssql.php | 145 -------- includes/db/DatabaseMysqlBase.php | 240 ------------ includes/db/DatabaseOracle.php | 54 --- includes/db/DatabasePostgres.php | 3 - includes/db/DatabaseSqlite.php | 42 --- includes/db/DatabaseUtility.php | 347 ------------------ .../database/{RBConnRef.php => DBConnRef.php} | 0 .../rdbms/database/position/DBMasterPos.php | 33 ++ .../database/position/MySQLMasterPos.php | 132 +++++++ .../resultwrapper/FakeResultWrapper.php | 81 ++++ .../resultwrapper/MssqlResultWrapper.php | 70 ++++ .../database/resultwrapper/ResultWrapper.php | 134 +++++++ includes/libs/rdbms/encasing/Blob.php | 19 + includes/libs/rdbms/encasing/LikeMatch.php | 28 ++ includes/libs/rdbms/encasing/MssqlBlob.php | 33 ++ includes/libs/rdbms/encasing/PostgresBlob.php | 4 + includes/libs/rdbms/field/Field.php | 30 ++ includes/libs/rdbms/field/MssqlField.php | 38 ++ includes/libs/rdbms/field/MySQLField.php | 106 ++++++ includes/libs/rdbms/field/ORAField.php | 50 +++ includes/libs/rdbms/field/SQLiteField.php | 39 ++ 22 files changed, 812 insertions(+), 846 deletions(-) delete mode 100644 includes/db/DatabaseUtility.php rename includes/libs/rdbms/database/{RBConnRef.php => DBConnRef.php} (100%) create mode 100644 includes/libs/rdbms/database/position/DBMasterPos.php create mode 100644 includes/libs/rdbms/database/position/MySQLMasterPos.php create mode 100644 includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php create mode 100644 includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php create mode 100644 includes/libs/rdbms/database/resultwrapper/ResultWrapper.php create mode 100644 includes/libs/rdbms/encasing/Blob.php create mode 100644 includes/libs/rdbms/encasing/LikeMatch.php create mode 100644 includes/libs/rdbms/encasing/MssqlBlob.php create mode 100644 includes/libs/rdbms/encasing/PostgresBlob.php create mode 100644 includes/libs/rdbms/field/Field.php create mode 100644 includes/libs/rdbms/field/MssqlField.php create mode 100644 includes/libs/rdbms/field/MySQLField.php create mode 100644 includes/libs/rdbms/field/ORAField.php create mode 100644 includes/libs/rdbms/field/SQLiteField.php diff --git a/autoload.php b/autoload.php index 759c715f2a..1ebf728ca2 100644 --- a/autoload.php +++ b/autoload.php @@ -189,7 +189,7 @@ $wgAutoloadLocalClasses = [ 'BitmapHandler' => __DIR__ . '/includes/media/Bitmap.php', 'BitmapHandler_ClientOnly' => __DIR__ . '/includes/media/Bitmap_ClientOnly.php', 'BitmapMetadataHandler' => __DIR__ . '/includes/media/BitmapMetadataHandler.php', - 'Blob' => __DIR__ . '/includes/db/DatabaseUtility.php', + 'Blob' => __DIR__ . '/includes/libs/rdbms/encasing/Blob.php', 'Block' => __DIR__ . '/includes/Block.php', 'BlockLevelPass' => __DIR__ . '/includes/parser/BlockLevelPass.php', 'BlockListPager' => __DIR__ . '/includes/specials/pagers/BlockListPager.php', @@ -299,13 +299,13 @@ $wgAutoloadLocalClasses = [ 'DBAccessBase' => __DIR__ . '/includes/dao/DBAccessBase.php', 'DBAccessError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBAccessObjectUtils' => __DIR__ . '/includes/dao/DBAccessObjectUtils.php', - 'DBConnRef' => __DIR__ . '/includes/libs/rdbms/database/RBConnRef.php', + 'DBConnRef' => __DIR__ . '/includes/libs/rdbms/database/DBConnRef.php', 'DBConnectionError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBExpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBFileJournal' => __DIR__ . '/includes/filebackend/filejournal/DBFileJournal.php', 'DBLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php', - 'DBMasterPos' => __DIR__ . '/includes/db/DatabaseUtility.php', + 'DBMasterPos' => __DIR__ . '/includes/libs/rdbms/database/position/DBMasterPos.php', 'DBQueryError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBReadOnlyError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBReplicationWaitError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', @@ -440,7 +440,7 @@ $wgAutoloadLocalClasses = [ 'FakeAuthTemplate' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php', 'FakeConverter' => __DIR__ . '/languages/FakeConverter.php', 'FakeMaintenance' => __DIR__ . '/maintenance/Maintenance.php', - 'FakeResultWrapper' => __DIR__ . '/includes/db/DatabaseUtility.php', + 'FakeResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php', 'FatalError' => __DIR__ . '/includes/exception/FatalError.php', 'FauxRequest' => __DIR__ . '/includes/FauxRequest.php', 'FauxResponse' => __DIR__ . '/includes/WebResponse.php', @@ -448,7 +448,7 @@ $wgAutoloadLocalClasses = [ 'FeedUtils' => __DIR__ . '/includes/FeedUtils.php', 'FetchText' => __DIR__ . '/maintenance/fetchText.php', 'FewestrevisionsPage' => __DIR__ . '/includes/specials/SpecialFewestrevisions.php', - 'Field' => __DIR__ . '/includes/db/DatabaseUtility.php', + 'Field' => __DIR__ . '/includes/libs/rdbms/field/Field.php', 'File' => __DIR__ . '/includes/filerepo/file/File.php', 'FileAwareNodeVisitor' => __DIR__ . '/maintenance/findDeprecated.php', 'FileBackend' => __DIR__ . '/includes/filebackend/FileBackend.php', @@ -715,7 +715,7 @@ $wgAutoloadLocalClasses = [ 'LegacyLogFormatter' => __DIR__ . '/includes/logging/LogFormatter.php', 'License' => __DIR__ . '/includes/Licenses.php', 'Licenses' => __DIR__ . '/includes/Licenses.php', - 'LikeMatch' => __DIR__ . '/includes/db/DatabaseUtility.php', + 'LikeMatch' => __DIR__ . '/includes/libs/rdbms/encasing/LikeMatch.php', 'LinkBatch' => __DIR__ . '/includes/cache/LinkBatch.php', 'LinkCache' => __DIR__ . '/includes/cache/LinkCache.php', 'LinkFilter' => __DIR__ . '/includes/LinkFilter.php', @@ -943,10 +943,10 @@ $wgAutoloadLocalClasses = [ 'MoveLogFormatter' => __DIR__ . '/includes/logging/MoveLogFormatter.php', 'MovePage' => __DIR__ . '/includes/MovePage.php', 'MovePageForm' => __DIR__ . '/includes/specials/SpecialMovepage.php', - 'MssqlBlob' => __DIR__ . '/includes/db/DatabaseMssql.php', - 'MssqlField' => __DIR__ . '/includes/db/DatabaseMssql.php', + 'MssqlBlob' => __DIR__ . '/includes/libs/rdbms/encasing/MssqlBlob.php', + 'MssqlField' => __DIR__ . '/includes/libs/rdbms/field/MssqlField.php', 'MssqlInstaller' => __DIR__ . '/includes/installer/MssqlInstaller.php', - 'MssqlResultWrapper' => __DIR__ . '/includes/db/DatabaseMssql.php', + 'MssqlResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php', 'MssqlUpdater' => __DIR__ . '/includes/installer/MssqlUpdater.php', 'MultiConfig' => __DIR__ . '/includes/config/MultiConfig.php', 'MultiHttpClient' => __DIR__ . '/includes/libs/MultiHttpClient.php', @@ -954,8 +954,8 @@ $wgAutoloadLocalClasses = [ 'MutableConfig' => __DIR__ . '/includes/config/MutableConfig.php', 'MutableContext' => __DIR__ . '/includes/context/MutableContext.php', 'MwSql' => __DIR__ . '/maintenance/sql.php', - 'MySQLField' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', - 'MySQLMasterPos' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', + 'MySQLField' => __DIR__ . '/includes/libs/rdbms/field/MySQLField.php', + 'MySQLMasterPos' => __DIR__ . '/includes/libs/rdbms/database/position/MySQLMasterPos.php', 'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/MySqlLockManager.php', 'MysqlInstaller' => __DIR__ . '/includes/installer/MysqlInstaller.php', 'MysqlUpdater' => __DIR__ . '/includes/installer/MysqlUpdater.php', @@ -980,7 +980,7 @@ $wgAutoloadLocalClasses = [ 'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php', 'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php', 'OOUIHTMLForm' => __DIR__ . '/includes/htmlform/OOUIHTMLForm.php', - 'ORAField' => __DIR__ . '/includes/db/DatabaseOracle.php', + 'ORAField' => __DIR__ . '/includes/libs/rdbms/field/ORAField.php', 'ORAResult' => __DIR__ . '/includes/db/DatabaseOracle.php', 'ObjectCache' => __DIR__ . '/includes/objectcache/ObjectCache.php', 'ObjectFactory' => __DIR__ . '/includes/libs/ObjectFactory.php', @@ -1065,7 +1065,7 @@ $wgAutoloadLocalClasses = [ 'PopulateRevisionLength' => __DIR__ . '/maintenance/populateRevisionLength.php', 'PopulateRevisionSha1' => __DIR__ . '/maintenance/populateRevisionSha1.php', 'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/PostgreSqlLockManager.php', - 'PostgresBlob' => __DIR__ . '/includes/db/DatabasePostgres.php', + 'PostgresBlob' => __DIR__ . '/includes/libs/rdbms/encasing/PostgresBlob.php', 'PostgresField' => __DIR__ . '/includes/db/DatabasePostgres.php', 'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php', 'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php', @@ -1182,7 +1182,7 @@ $wgAutoloadLocalClasses = [ 'ResourceLoaderUserTokensModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserTokensModule.php', 'ResourceLoaderWikiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderWikiModule.php', 'RestbaseVirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/RestbaseVirtualRESTService.php', - 'ResultWrapper' => __DIR__ . '/includes/db/DatabaseUtility.php', + 'ResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php', 'RevDelArchiveItem' => __DIR__ . '/includes/revisiondelete/RevDelArchiveItem.php', 'RevDelArchiveList' => __DIR__ . '/includes/revisiondelete/RevDelArchiveList.php', 'RevDelArchivedFileItem' => __DIR__ . '/includes/revisiondelete/RevDelArchivedFileItem.php', @@ -1214,7 +1214,7 @@ $wgAutoloadLocalClasses = [ 'RowUpdateGenerator' => __DIR__ . '/includes/utils/RowUpdateGenerator.php', 'RunJobs' => __DIR__ . '/maintenance/runJobs.php', 'RunningStat' => __DIR__ . '/includes/compat/RunningStatCompat.php', - 'SQLiteField' => __DIR__ . '/includes/db/DatabaseSqlite.php', + 'SQLiteField' => __DIR__ . '/includes/libs/rdbms/field/SQLiteField.php', 'SVGMetadataExtractor' => __DIR__ . '/includes/media/SVGMetadataExtractor.php', 'SVGReader' => __DIR__ . '/includes/media/SVGMetadataExtractor.php', 'SamplingStatsdClient' => __DIR__ . '/includes/libs/SamplingStatsdClient.php', diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 2439c60073..1d7adc6856 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -1410,148 +1410,3 @@ class DatabaseMssql extends Database { return wfSetVar( $this->mIgnoreErrors, $value ); } } // end DatabaseMssql class - -/** - * Utility class. - * - * @ingroup Database - */ -class MssqlField implements Field { - private $name, $tableName, $default, $max_length, $nullable, $type; - - function __construct( $info ) { - $this->name = $info['COLUMN_NAME']; - $this->tableName = $info['TABLE_NAME']; - $this->default = $info['COLUMN_DEFAULT']; - $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; - $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' ); - $this->type = $info['DATA_TYPE']; - } - - function name() { - return $this->name; - } - - function tableName() { - return $this->tableName; - } - - function defaultValue() { - return $this->default; - } - - function maxLength() { - return $this->max_length; - } - - function isNullable() { - return $this->nullable; - } - - function type() { - return $this->type; - } -} - -class MssqlBlob extends Blob { - public function __construct( $data ) { - if ( $data instanceof MssqlBlob ) { - return $data; - } elseif ( $data instanceof Blob ) { - $this->mData = $data->fetch(); - } elseif ( is_array( $data ) && is_object( $data ) ) { - $this->mData = serialize( $data ); - } else { - $this->mData = $data; - } - } - - /** - * Returns an unquoted hex representation of a binary string - * for insertion into varbinary-type fields - * @return string - */ - public function fetch() { - if ( $this->mData === null ) { - return 'null'; - } - - $ret = '0x'; - $dataLength = strlen( $this->mData ); - for ( $i = 0; $i < $dataLength; $i++ ) { - $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) ); - } - - return $ret; - } -} - -class MssqlResultWrapper extends ResultWrapper { - private $mSeekTo = null; - - /** - * @return stdClass|bool - */ - public function fetchObject() { - $res = $this->result; - - if ( $this->mSeekTo !== null ) { - $result = sqlsrv_fetch_object( $res, 'stdClass', [], - SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); - $this->mSeekTo = null; - } else { - $result = sqlsrv_fetch_object( $res ); - } - - // MediaWiki expects us to return boolean false when there are no more rows instead of null - if ( $result === null ) { - return false; - } - - return $result; - } - - /** - * @return array|bool - */ - public function fetchRow() { - $res = $this->result; - - if ( $this->mSeekTo !== null ) { - $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH, - SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); - $this->mSeekTo = null; - } else { - $result = sqlsrv_fetch_array( $res ); - } - - // MediaWiki expects us to return boolean false when there are no more rows instead of null - if ( $result === null ) { - return false; - } - - return $result; - } - - /** - * @param int $row - * @return bool - */ - public function seek( $row ) { - $res = $this->result; - - // check bounds - $numRows = $this->db->numRows( $res ); - $row = intval( $row ); - - if ( $numRows === 0 ) { - return false; - } elseif ( $row < 0 || $row > $numRows - 1 ) { - return false; - } - - // Unlike MySQL, the seek actually happens on the next access - $this->mSeekTo = $row; - return true; - } -} diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php index af80d24aef..f8737a84df 100644 --- a/includes/db/DatabaseMysqlBase.php +++ b/includes/db/DatabaseMysqlBase.php @@ -1351,243 +1351,3 @@ abstract class DatabaseMysqlBase extends Database { } } -/** - * Utility class. - * @ingroup Database - */ -class MySQLField implements Field { - private $name, $tablename, $default, $max_length, $nullable, - $is_pk, $is_unique, $is_multiple, $is_key, $type, $binary, - $is_numeric, $is_blob, $is_unsigned, $is_zerofill; - - function __construct( $info ) { - $this->name = $info->name; - $this->tablename = $info->table; - $this->default = $info->def; - $this->max_length = $info->max_length; - $this->nullable = !$info->not_null; - $this->is_pk = $info->primary_key; - $this->is_unique = $info->unique_key; - $this->is_multiple = $info->multiple_key; - $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple ); - $this->type = $info->type; - $this->binary = isset( $info->binary ) ? $info->binary : false; - $this->is_numeric = isset( $info->numeric ) ? $info->numeric : false; - $this->is_blob = isset( $info->blob ) ? $info->blob : false; - $this->is_unsigned = isset( $info->unsigned ) ? $info->unsigned : false; - $this->is_zerofill = isset( $info->zerofill ) ? $info->zerofill : false; - } - - /** - * @return string - */ - function name() { - return $this->name; - } - - /** - * @return string - */ - function tableName() { - return $this->tablename; - } - - /** - * @return string - */ - function type() { - return $this->type; - } - - /** - * @return bool - */ - function isNullable() { - return $this->nullable; - } - - function defaultValue() { - return $this->default; - } - - /** - * @return bool - */ - function isKey() { - return $this->is_key; - } - - /** - * @return bool - */ - function isMultipleKey() { - return $this->is_multiple; - } - - /** - * @return bool - */ - function isBinary() { - return $this->binary; - } - - /** - * @return bool - */ - function isNumeric() { - return $this->is_numeric; - } - - /** - * @return bool - */ - function isBlob() { - return $this->is_blob; - } - - /** - * @return bool - */ - function isUnsigned() { - return $this->is_unsigned; - } - - /** - * @return bool - */ - function isZerofill() { - return $this->is_zerofill; - } -} - -/** - * DBMasterPos class for MySQL/MariaDB - * - * Note that master positions and sync logic here make some assumptions: - * - Binlog-based usage assumes single-source replication and non-hierarchical replication. - * - GTID-based usage allows getting/syncing with multi-source replication. It is assumed - * that GTID sets are complete (e.g. include all domains on the server). - */ -class MySQLMasterPos implements DBMasterPos { - /** @var string Binlog file */ - public $file; - /** @var int Binglog file position */ - public $pos; - /** @var string[] GTID list */ - public $gtids = []; - /** @var float UNIX timestamp */ - public $asOfTime = 0.0; - - /** - * @param string $file Binlog file name - * @param integer $pos Binlog position - * @param string $gtid Comma separated GTID set [optional] - */ - function __construct( $file, $pos, $gtid = '' ) { - $this->file = $file; - $this->pos = $pos; - $this->gtids = array_map( 'trim', explode( ',', $gtid ) ); - $this->asOfTime = microtime( true ); - } - - /** - * @return string /, e.g db1034-bin.000976/843431247 - */ - function __toString() { - return "{$this->file}/{$this->pos}"; - } - - function asOfTime() { - return $this->asOfTime; - } - - function hasReached( DBMasterPos $pos ) { - if ( !( $pos instanceof self ) ) { - throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ ); - } - - // Prefer GTID comparisons, which work with multi-tier replication - $thisPosByDomain = $this->getGtidCoordinates(); - $thatPosByDomain = $pos->getGtidCoordinates(); - if ( $thisPosByDomain && $thatPosByDomain ) { - $reached = true; - // Check that this has positions GTE all of those in $pos for all domains in $pos - foreach ( $thatPosByDomain as $domain => $thatPos ) { - $thisPos = isset( $thisPosByDomain[$domain] ) ? $thisPosByDomain[$domain] : -1; - $reached = $reached && ( $thatPos <= $thisPos ); - } - - return $reached; - } - - // Fallback to the binlog file comparisons - $thisBinPos = $this->getBinlogCoordinates(); - $thatBinPos = $pos->getBinlogCoordinates(); - if ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ) { - return ( $thisBinPos['pos'] >= $thatBinPos['pos'] ); - } - - // Comparing totally different binlogs does not make sense - return false; - } - - function channelsMatch( DBMasterPos $pos ) { - if ( !( $pos instanceof self ) ) { - throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ ); - } - - // Prefer GTID comparisons, which work with multi-tier replication - $thisPosDomains = array_keys( $this->getGtidCoordinates() ); - $thatPosDomains = array_keys( $pos->getGtidCoordinates() ); - if ( $thisPosDomains && $thatPosDomains ) { - // Check that this has GTIDs for all domains in $pos - return !array_diff( $thatPosDomains, $thisPosDomains ); - } - - // Fallback to the binlog file comparisons - $thisBinPos = $this->getBinlogCoordinates(); - $thatBinPos = $pos->getBinlogCoordinates(); - - return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ); - } - - /** - * @note: this returns false for multi-source replication GTID sets - * @see https://mariadb.com/kb/en/mariadb/gtid - * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html - * @return array Map of (domain => integer position) or false - */ - protected function getGtidCoordinates() { - $gtidInfos = []; - foreach ( $this->gtids as $gtid ) { - $m = []; - // MariaDB style: -- - if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) { - $gtidInfos[(int)$m[1]] = (int)$m[2]; - // MySQL style: : - } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) { - $gtidInfos[$m[1]] = (int)$m[2]; - } else { - $gtidInfos = []; - break; // unrecognized GTID - } - - } - - return $gtidInfos; - } - - /** - * @see http://dev.mysql.com/doc/refman/5.7/en/show-master-status.html - * @see http://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html - * @return array|bool (binlog, (integer file number, integer position)) or false - */ - protected function getBinlogCoordinates() { - $m = []; - if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) { - return [ 'binlog' => $m[1], 'pos' => [ (int)$m[2], (int)$m[3] ] ]; - } - - return false; - } -} diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 5d0ff447ba..df311aafac 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -128,60 +128,6 @@ class ORAResult { } } -/** - * Utility class. - * @ingroup Database - */ -class ORAField implements Field { - private $name, $tablename, $default, $max_length, $nullable, - $is_pk, $is_unique, $is_multiple, $is_key, $type; - - function __construct( $info ) { - $this->name = $info['column_name']; - $this->tablename = $info['table_name']; - $this->default = $info['data_default']; - $this->max_length = $info['data_length']; - $this->nullable = $info['not_null']; - $this->is_pk = isset( $info['prim'] ) && $info['prim'] == 1 ? 1 : 0; - $this->is_unique = isset( $info['uniq'] ) && $info['uniq'] == 1 ? 1 : 0; - $this->is_multiple = isset( $info['nonuniq'] ) && $info['nonuniq'] == 1 ? 1 : 0; - $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple ); - $this->type = $info['data_type']; - } - - function name() { - return $this->name; - } - - function tableName() { - return $this->tablename; - } - - function defaultValue() { - return $this->default; - } - - function maxLength() { - return $this->max_length; - } - - function isNullable() { - return $this->nullable; - } - - function isKey() { - return $this->is_key; - } - - function isMultipleKey() { - return $this->is_multiple; - } - - function type() { - return $this->type; - } -} - /** * @ingroup Database */ diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index b1cc96bc7c..590e1f4931 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -1636,6 +1636,3 @@ SQL; return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 ); } } // end DatabasePostgres class - -class PostgresBlob extends Blob { -} diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index 6bf48e2365..fa4e453b65 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -1044,45 +1044,3 @@ class DatabaseSqlite extends Database { } } // end DatabaseSqlite class - -/** - * @ingroup Database - */ -class SQLiteField implements Field { - private $info, $tableName; - - function __construct( $info, $tableName ) { - $this->info = $info; - $this->tableName = $tableName; - } - - function name() { - return $this->info->name; - } - - function tableName() { - return $this->tableName; - } - - function defaultValue() { - if ( is_string( $this->info->dflt_value ) ) { - // Typically quoted - if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) { - return str_replace( "''", "'", $this->info->dflt_value ); - } - } - - return $this->info->dflt_value; - } - - /** - * @return bool - */ - function isNullable() { - return !$this->info->notnull; - } - - function type() { - return $this->info->type; - } -} // end SQLiteField diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php deleted file mode 100644 index 2b8c8c6d4b..0000000000 --- a/includes/db/DatabaseUtility.php +++ /dev/null @@ -1,347 +0,0 @@ -mData = $data; - } - - function fetch() { - return $this->mData; - } -} - -/** - * Base for all database-specific classes representing information about database fields - * @ingroup Database - */ -interface Field { - /** - * Field name - * @return string - */ - function name(); - - /** - * Name of table this field belongs to - * @return string - */ - function tableName(); - - /** - * Database type - * @return string - */ - function type(); - - /** - * Whether this field can store NULL values - * @return bool - */ - function isNullable(); -} - -/** - * Result wrapper for grabbing data queried by someone else - * @ingroup Database - */ -class ResultWrapper implements Iterator { - /** @var resource */ - public $result; - - /** @var IDatabase */ - protected $db; - - /** @var int */ - protected $pos = 0; - - /** @var object|null */ - protected $currentRow = null; - - /** - * Create a new result object from a result resource and a Database object - * - * @param IDatabase $database - * @param resource|ResultWrapper $result - */ - function __construct( $database, $result ) { - $this->db = $database; - - if ( $result instanceof ResultWrapper ) { - $this->result = $result->result; - } else { - $this->result = $result; - } - } - - /** - * Get the number of rows in a result object - * - * @return int - */ - function numRows() { - return $this->db->numRows( $this ); - } - - /** - * Fetch the next row from the given result object, in object form. Fields can be retrieved with - * $row->fieldname, with fields acting like member variables. If no more rows are available, - * false is returned. - * - * @return stdClass|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchObject() { - return $this->db->fetchObject( $this ); - } - - /** - * Fetch the next row from the given result object, in associative array form. Fields are - * retrieved with $row['fieldname']. If no more rows are available, false is returned. - * - * @return array|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchRow() { - return $this->db->fetchRow( $this ); - } - - /** - * Free a result object - */ - function free() { - $this->db->freeResult( $this ); - unset( $this->result ); - unset( $this->db ); - } - - /** - * Change the position of the cursor in a result object. - * See mysql_data_seek() - * - * @param int $row - */ - function seek( $row ) { - $this->db->dataSeek( $this, $row ); - } - - /* - * ======= Iterator functions ======= - * Note that using these in combination with the non-iterator functions - * above may cause rows to be skipped or repeated. - */ - - function rewind() { - if ( $this->numRows() ) { - $this->db->dataSeek( $this, 0 ); - } - $this->pos = 0; - $this->currentRow = null; - } - - /** - * @return stdClass|array|bool - */ - function current() { - if ( is_null( $this->currentRow ) ) { - $this->next(); - } - - return $this->currentRow; - } - - /** - * @return int - */ - function key() { - return $this->pos; - } - - /** - * @return stdClass - */ - function next() { - $this->pos++; - $this->currentRow = $this->fetchObject(); - - return $this->currentRow; - } - - /** - * @return bool - */ - function valid() { - return $this->current() !== false; - } -} - -/** - * Overloads the relevant methods of the real ResultsWrapper so it - * doesn't go anywhere near an actual database. - */ -class FakeResultWrapper extends ResultWrapper { - /** @var array */ - public $result = []; - - /** @var null And it's going to stay that way :D */ - protected $db = null; - - /** @var int */ - protected $pos = 0; - - /** @var array|stdClass|bool */ - protected $currentRow = null; - - /** - * @param array $array - */ - function __construct( $array ) { - $this->result = $array; - } - - /** - * @return int - */ - function numRows() { - return count( $this->result ); - } - - /** - * @return array|bool - */ - function fetchRow() { - if ( $this->pos < count( $this->result ) ) { - $this->currentRow = $this->result[$this->pos]; - } else { - $this->currentRow = false; - } - $this->pos++; - if ( is_object( $this->currentRow ) ) { - return get_object_vars( $this->currentRow ); - } else { - return $this->currentRow; - } - } - - function seek( $row ) { - $this->pos = $row; - } - - function free() { - } - - /** - * Callers want to be able to access fields with $this->fieldName - * @return bool|stdClass - */ - function fetchObject() { - $this->fetchRow(); - if ( $this->currentRow ) { - return (object)$this->currentRow; - } else { - return false; - } - } - - function rewind() { - $this->pos = 0; - $this->currentRow = null; - } - - /** - * @return bool|stdClass - */ - function next() { - return $this->fetchObject(); - } -} - -/** - * Used by DatabaseBase::buildLike() to represent characters that have special - * meaning in SQL LIKE clauses and thus need no escaping. Don't instantiate it - * manually, use DatabaseBase::anyChar() and anyString() instead. - */ -class LikeMatch { - /** @var string */ - private $str; - - /** - * Store a string into a LikeMatch marker object. - * - * @param string $s - */ - public function __construct( $s ) { - $this->str = $s; - } - - /** - * Return the original stored string. - * - * @return string - */ - public function toString() { - return $this->str; - } -} - -/** - * An object representing a master or replica DB position in a replicated setup. - * - * The implementation details of this opaque type are up to the database subclass. - */ -interface DBMasterPos { - /** - * @return float UNIX timestamp - * @since 1.25 - */ - public function asOfTime(); - - /** - * @param DBMasterPos $pos - * @return bool Whether this position is at or higher than $pos - * @since 1.27 - */ - public function hasReached( DBMasterPos $pos ); - - /** - * @param DBMasterPos $pos - * @return bool Whether this position appears to be for the same channel as another - * @since 1.27 - */ - public function channelsMatch( DBMasterPos $pos ); - - /** - * @return string - * @since 1.27 - */ - public function __toString(); -} diff --git a/includes/libs/rdbms/database/RBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php similarity index 100% rename from includes/libs/rdbms/database/RBConnRef.php rename to includes/libs/rdbms/database/DBConnRef.php diff --git a/includes/libs/rdbms/database/position/DBMasterPos.php b/includes/libs/rdbms/database/position/DBMasterPos.php new file mode 100644 index 0000000000..eda0ff3256 --- /dev/null +++ b/includes/libs/rdbms/database/position/DBMasterPos.php @@ -0,0 +1,33 @@ +file = $file; + $this->pos = $pos; + $this->gtids = array_map( 'trim', explode( ',', $gtid ) ); + $this->asOfTime = microtime( true ); + } + + /** + * @return string /, e.g db1034-bin.000976/843431247 + */ + function __toString() { + return "{$this->file}/{$this->pos}"; + } + + function asOfTime() { + return $this->asOfTime; + } + + function hasReached( DBMasterPos $pos ) { + if ( !( $pos instanceof self ) ) { + throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ ); + } + + // Prefer GTID comparisons, which work with multi-tier replication + $thisPosByDomain = $this->getGtidCoordinates(); + $thatPosByDomain = $pos->getGtidCoordinates(); + if ( $thisPosByDomain && $thatPosByDomain ) { + $reached = true; + // Check that this has positions GTE all of those in $pos for all domains in $pos + foreach ( $thatPosByDomain as $domain => $thatPos ) { + $thisPos = isset( $thisPosByDomain[$domain] ) ? $thisPosByDomain[$domain] : -1; + $reached = $reached && ( $thatPos <= $thisPos ); + } + + return $reached; + } + + // Fallback to the binlog file comparisons + $thisBinPos = $this->getBinlogCoordinates(); + $thatBinPos = $pos->getBinlogCoordinates(); + if ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ) { + return ( $thisBinPos['pos'] >= $thatBinPos['pos'] ); + } + + // Comparing totally different binlogs does not make sense + return false; + } + + function channelsMatch( DBMasterPos $pos ) { + if ( !( $pos instanceof self ) ) { + throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ ); + } + + // Prefer GTID comparisons, which work with multi-tier replication + $thisPosDomains = array_keys( $this->getGtidCoordinates() ); + $thatPosDomains = array_keys( $pos->getGtidCoordinates() ); + if ( $thisPosDomains && $thatPosDomains ) { + // Check that this has GTIDs for all domains in $pos + return !array_diff( $thatPosDomains, $thisPosDomains ); + } + + // Fallback to the binlog file comparisons + $thisBinPos = $this->getBinlogCoordinates(); + $thatBinPos = $pos->getBinlogCoordinates(); + + return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ); + } + + /** + * @note: this returns false for multi-source replication GTID sets + * @see https://mariadb.com/kb/en/mariadb/gtid + * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html + * @return array Map of (domain => integer position) or false + */ + protected function getGtidCoordinates() { + $gtidInfos = []; + foreach ( $this->gtids as $gtid ) { + $m = []; + // MariaDB style: -- + if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) { + $gtidInfos[(int)$m[1]] = (int)$m[2]; + // MySQL style: : + } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) { + $gtidInfos[$m[1]] = (int)$m[2]; + } else { + $gtidInfos = []; + break; // unrecognized GTID + } + + } + + return $gtidInfos; + } + + /** + * @see http://dev.mysql.com/doc/refman/5.7/en/show-master-status.html + * @see http://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html + * @return array|bool (binlog, (integer file number, integer position)) or false + */ + protected function getBinlogCoordinates() { + $m = []; + if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) { + return [ 'binlog' => $m[1], 'pos' => [ (int)$m[2], (int)$m[3] ] ]; + } + + return false; + } +} diff --git a/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php new file mode 100644 index 0000000000..774def8086 --- /dev/null +++ b/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php @@ -0,0 +1,81 @@ +result = $array; + } + + /** + * @return int + */ + function numRows() { + return count( $this->result ); + } + + /** + * @return array|bool + */ + function fetchRow() { + if ( $this->pos < count( $this->result ) ) { + $this->currentRow = $this->result[$this->pos]; + } else { + $this->currentRow = false; + } + $this->pos++; + if ( is_object( $this->currentRow ) ) { + return get_object_vars( $this->currentRow ); + } else { + return $this->currentRow; + } + } + + function seek( $row ) { + $this->pos = $row; + } + + function free() { + } + + /** + * Callers want to be able to access fields with $this->fieldName + * @return bool|stdClass + */ + function fetchObject() { + $this->fetchRow(); + if ( $this->currentRow ) { + return (object)$this->currentRow; + } else { + return false; + } + } + + function rewind() { + $this->pos = 0; + $this->currentRow = null; + } + + /** + * @return bool|stdClass + */ + function next() { + return $this->fetchObject(); + } +} diff --git a/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php new file mode 100644 index 0000000000..cccb8f17d2 --- /dev/null +++ b/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php @@ -0,0 +1,70 @@ +result; + + if ( $this->mSeekTo !== null ) { + $result = sqlsrv_fetch_object( $res, 'stdClass', [], + SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); + $this->mSeekTo = null; + } else { + $result = sqlsrv_fetch_object( $res ); + } + + // MediaWiki expects us to return boolean false when there are no more rows instead of null + if ( $result === null ) { + return false; + } + + return $result; + } + + /** + * @return array|bool + */ + public function fetchRow() { + $res = $this->result; + + if ( $this->mSeekTo !== null ) { + $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH, + SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); + $this->mSeekTo = null; + } else { + $result = sqlsrv_fetch_array( $res ); + } + + // MediaWiki expects us to return boolean false when there are no more rows instead of null + if ( $result === null ) { + return false; + } + + return $result; + } + + /** + * @param int $row + * @return bool + */ + public function seek( $row ) { + $res = $this->result; + + // check bounds + $numRows = $this->db->numRows( $res ); + $row = intval( $row ); + + if ( $numRows === 0 ) { + return false; + } elseif ( $row < 0 || $row > $numRows - 1 ) { + return false; + } + + // Unlike MySQL, the seek actually happens on the next access + $this->mSeekTo = $row; + return true; + } +} diff --git a/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php new file mode 100644 index 0000000000..252f4f7e79 --- /dev/null +++ b/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php @@ -0,0 +1,134 @@ +db = $database; + + if ( $result instanceof ResultWrapper ) { + $this->result = $result->result; + } else { + $this->result = $result; + } + } + + /** + * Get the number of rows in a result object + * + * @return int + */ + function numRows() { + return $this->db->numRows( $this ); + } + + /** + * Fetch the next row from the given result object, in object form. Fields can be retrieved with + * $row->fieldname, with fields acting like member variables. If no more rows are available, + * false is returned. + * + * @return stdClass|bool + * @throws DBUnexpectedError Thrown if the database returns an error + */ + function fetchObject() { + return $this->db->fetchObject( $this ); + } + + /** + * Fetch the next row from the given result object, in associative array form. Fields are + * retrieved with $row['fieldname']. If no more rows are available, false is returned. + * + * @return array|bool + * @throws DBUnexpectedError Thrown if the database returns an error + */ + function fetchRow() { + return $this->db->fetchRow( $this ); + } + + /** + * Free a result object + */ + function free() { + $this->db->freeResult( $this ); + unset( $this->result ); + unset( $this->db ); + } + + /** + * Change the position of the cursor in a result object. + * See mysql_data_seek() + * + * @param int $row + */ + function seek( $row ) { + $this->db->dataSeek( $this, $row ); + } + + /* + * ======= Iterator functions ======= + * Note that using these in combination with the non-iterator functions + * above may cause rows to be skipped or repeated. + */ + + function rewind() { + if ( $this->numRows() ) { + $this->db->dataSeek( $this, 0 ); + } + $this->pos = 0; + $this->currentRow = null; + } + + /** + * @return stdClass|array|bool + */ + function current() { + if ( is_null( $this->currentRow ) ) { + $this->next(); + } + + return $this->currentRow; + } + + /** + * @return int + */ + function key() { + return $this->pos; + } + + /** + * @return stdClass + */ + function next() { + $this->pos++; + $this->currentRow = $this->fetchObject(); + + return $this->currentRow; + } + + /** + * @return bool + */ + function valid() { + return $this->current() !== false; + } +} diff --git a/includes/libs/rdbms/encasing/Blob.php b/includes/libs/rdbms/encasing/Blob.php new file mode 100644 index 0000000000..bd90330577 --- /dev/null +++ b/includes/libs/rdbms/encasing/Blob.php @@ -0,0 +1,19 @@ +mData = $data; + } + + function fetch() { + return $this->mData; + } +} diff --git a/includes/libs/rdbms/encasing/LikeMatch.php b/includes/libs/rdbms/encasing/LikeMatch.php new file mode 100644 index 0000000000..5dee884f28 --- /dev/null +++ b/includes/libs/rdbms/encasing/LikeMatch.php @@ -0,0 +1,28 @@ +str = $s; + } + + /** + * Return the original stored string. + * + * @return string + */ + public function toString() { + return $this->str; + } +} diff --git a/includes/libs/rdbms/encasing/MssqlBlob.php b/includes/libs/rdbms/encasing/MssqlBlob.php new file mode 100644 index 0000000000..35be65c26e --- /dev/null +++ b/includes/libs/rdbms/encasing/MssqlBlob.php @@ -0,0 +1,33 @@ +mData = $data->fetch(); + } elseif ( is_array( $data ) && is_object( $data ) ) { + $this->mData = serialize( $data ); + } else { + $this->mData = $data; + } + } + + /** + * Returns an unquoted hex representation of a binary string + * for insertion into varbinary-type fields + * @return string + */ + public function fetch() { + if ( $this->mData === null ) { + return 'null'; + } + + $ret = '0x'; + $dataLength = strlen( $this->mData ); + for ( $i = 0; $i < $dataLength; $i++ ) { + $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) ); + } + + return $ret; + } +} diff --git a/includes/libs/rdbms/encasing/PostgresBlob.php b/includes/libs/rdbms/encasing/PostgresBlob.php new file mode 100644 index 0000000000..cc52336c29 --- /dev/null +++ b/includes/libs/rdbms/encasing/PostgresBlob.php @@ -0,0 +1,4 @@ +name = $info['COLUMN_NAME']; + $this->tableName = $info['TABLE_NAME']; + $this->default = $info['COLUMN_DEFAULT']; + $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; + $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' ); + $this->type = $info['DATA_TYPE']; + } + + function name() { + return $this->name; + } + + function tableName() { + return $this->tableName; + } + + function defaultValue() { + return $this->default; + } + + function maxLength() { + return $this->max_length; + } + + function isNullable() { + return $this->nullable; + } + + function type() { + return $this->type; + } +} + diff --git a/includes/libs/rdbms/field/MySQLField.php b/includes/libs/rdbms/field/MySQLField.php new file mode 100644 index 0000000000..8cf964cc3d --- /dev/null +++ b/includes/libs/rdbms/field/MySQLField.php @@ -0,0 +1,106 @@ +name = $info->name; + $this->tablename = $info->table; + $this->default = $info->def; + $this->max_length = $info->max_length; + $this->nullable = !$info->not_null; + $this->is_pk = $info->primary_key; + $this->is_unique = $info->unique_key; + $this->is_multiple = $info->multiple_key; + $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple ); + $this->type = $info->type; + $this->binary = isset( $info->binary ) ? $info->binary : false; + $this->is_numeric = isset( $info->numeric ) ? $info->numeric : false; + $this->is_blob = isset( $info->blob ) ? $info->blob : false; + $this->is_unsigned = isset( $info->unsigned ) ? $info->unsigned : false; + $this->is_zerofill = isset( $info->zerofill ) ? $info->zerofill : false; + } + + /** + * @return string + */ + function name() { + return $this->name; + } + + /** + * @return string + */ + function tableName() { + return $this->tablename; + } + + /** + * @return string + */ + function type() { + return $this->type; + } + + /** + * @return bool + */ + function isNullable() { + return $this->nullable; + } + + function defaultValue() { + return $this->default; + } + + /** + * @return bool + */ + function isKey() { + return $this->is_key; + } + + /** + * @return bool + */ + function isMultipleKey() { + return $this->is_multiple; + } + + /** + * @return bool + */ + function isBinary() { + return $this->binary; + } + + /** + * @return bool + */ + function isNumeric() { + return $this->is_numeric; + } + + /** + * @return bool + */ + function isBlob() { + return $this->is_blob; + } + + /** + * @return bool + */ + function isUnsigned() { + return $this->is_unsigned; + } + + /** + * @return bool + */ + function isZerofill() { + return $this->is_zerofill; + } +} + diff --git a/includes/libs/rdbms/field/ORAField.php b/includes/libs/rdbms/field/ORAField.php new file mode 100644 index 0000000000..e48310ddc3 --- /dev/null +++ b/includes/libs/rdbms/field/ORAField.php @@ -0,0 +1,50 @@ +name = $info['column_name']; + $this->tablename = $info['table_name']; + $this->default = $info['data_default']; + $this->max_length = $info['data_length']; + $this->nullable = $info['not_null']; + $this->is_pk = isset( $info['prim'] ) && $info['prim'] == 1 ? 1 : 0; + $this->is_unique = isset( $info['uniq'] ) && $info['uniq'] == 1 ? 1 : 0; + $this->is_multiple = isset( $info['nonuniq'] ) && $info['nonuniq'] == 1 ? 1 : 0; + $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple ); + $this->type = $info['data_type']; + } + + function name() { + return $this->name; + } + + function tableName() { + return $this->tablename; + } + + function defaultValue() { + return $this->default; + } + + function maxLength() { + return $this->max_length; + } + + function isNullable() { + return $this->nullable; + } + + function isKey() { + return $this->is_key; + } + + function isMultipleKey() { + return $this->is_multiple; + } + + function type() { + return $this->type; + } +} diff --git a/includes/libs/rdbms/field/SQLiteField.php b/includes/libs/rdbms/field/SQLiteField.php new file mode 100644 index 0000000000..0a2389bfb0 --- /dev/null +++ b/includes/libs/rdbms/field/SQLiteField.php @@ -0,0 +1,39 @@ +info = $info; + $this->tableName = $tableName; + } + + function name() { + return $this->info->name; + } + + function tableName() { + return $this->tableName; + } + + function defaultValue() { + if ( is_string( $this->info->dflt_value ) ) { + // Typically quoted + if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) { + return str_replace( "''", "'", $this->info->dflt_value ); + } + } + + return $this->info->dflt_value; + } + + /** + * @return bool + */ + function isNullable() { + return !$this->info->notnull; + } + + function type() { + return $this->info->type; + } +} -- 2.20.1