diff options
author | Luke Shumaker <LukeShu@sbcglobal.net> | 2014-01-28 09:50:25 -0500 |
---|---|---|
committer | Luke Shumaker <LukeShu@sbcglobal.net> | 2014-01-28 09:50:25 -0500 |
commit | 5744df39e15f85c6cc8a9faf8924d77e76d2b216 (patch) | |
tree | a8c8dd40a94d1fa0d5377566aa5548ae55a163da /includes/db | |
parent | 4bb2aeca1d198391ca856aa16c40b8559c68daec (diff) | |
parent | 224b22a051051f6c2e494c3a2fb4adb42898e2d1 (diff) |
Merge branch 'archwiki'
Conflicts:
extensions/FluxBBAuthPlugin.php
extensions/SyntaxHighlight_GeSHi/README
extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php
extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.i18n.php
extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.php
extensions/SyntaxHighlight_GeSHi/geshi/docs/CHANGES
extensions/SyntaxHighlight_GeSHi/geshi/docs/THANKS
extensions/SyntaxHighlight_GeSHi/geshi/docs/TODO
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractClass.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractClass_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractMethod.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractPrivateClass.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractPrivateClass_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractPrivateMethod.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Class.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Class_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Constant.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Constructor.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Destructor.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Function.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Global.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/I.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Index.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Interface.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Interface_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/L.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Lminus.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Lplus.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Method.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Page.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Page_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/PrivateClass.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/PrivateClass_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/PrivateMethod.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/PrivateVariable.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/StaticMethod.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/StaticVariable.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/T.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Tminus.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Tplus.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Variable.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/blank.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/class_folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/file.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/function_folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/next_button.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/next_button_disabled.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/package.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/package_folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/previous_button.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/previous_button_disabled.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/private_class_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/tutorial.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/tutorial_folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/up_button.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/geshi-doc.html
extensions/SyntaxHighlight_GeSHi/geshi/docs/geshi-doc.txt
extensions/SyntaxHighlight_GeSHi/geshi/geshi.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/4cs.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/6502acme.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/6502kickass.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/6502tasm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/68000devpac.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/abap.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/actionscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/actionscript3.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ada.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/algol68.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/apache.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/applescript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/apt_sources.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/asm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/asp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/autoconf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/autohotkey.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/autoit.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/avisynth.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/awk.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bascomavr.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bash.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/basic4gl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bibtex.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/blitzbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bnf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/boo.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/c.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/c_loadrunner.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/c_mac.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/caddcl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cadlisp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cfdg.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cfm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/chaiscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cil.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/clojure.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cmake.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cobol.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/coffeescript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cpp-qt.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cpp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/csharp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/css.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cuesheet.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/d.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/dcs.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/delphi.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/diff.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/div.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/dos.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/dot.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/e.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ecmascript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/eiffel.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/email.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/epc.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/erlang.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/euphoria.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/f1.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/falcon.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/fo.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/fortran.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/freebasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/fsharp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gambas.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gdb.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/genero.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/genie.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gettext.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/glsl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gnuplot.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/go.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/groovy.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gwbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/haskell.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/hicest.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/hq9plus.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/html4strict.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/html5.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/icon.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/idl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ini.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/inno.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/intercal.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/io.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/j.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/java.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/java5.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/javascript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/jquery.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/kixtart.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/klonec.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/klonecpp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/latex.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lb.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lisp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/llvm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/locobasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/logtalk.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lolcode.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lotusformulas.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lotusscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lsl2.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lua.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/m68k.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/magiksf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/make.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mapbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/matlab.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mirc.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mmix.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/modula2.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/modula3.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mpasm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mxml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mysql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/newlisp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/nsis.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oberon2.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/objc.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/objeck.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ocaml-brief.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ocaml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oobas.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oracle11.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oracle8.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oxygene.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oz.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pascal.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pcre.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/per.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/perl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/perl6.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/php-brief.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/php.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pic16.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pike.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pixelbender.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pli.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/plsql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/postgresql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/povray.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/powerbuilder.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/powershell.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/proftpd.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/progress.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/prolog.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/properties.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/providex.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/purebasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pycon.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/python.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/q.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/qbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/rails.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/rebol.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/reg.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/robots.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/rpmspec.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/rsplus.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ruby.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/sas.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/scala.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/scheme.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/scilab.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/sdlbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/smalltalk.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/smarty.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/sql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/systemverilog.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/tcl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/teraterm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/text.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/thinbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/tsql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/typoscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/unicon.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/uscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vala.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vb.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vbnet.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/verilog.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vhdl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vim.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/visualfoxpro.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/visualprolog.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/whitespace.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/whois.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/winbatch.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/xbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/xml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/xorg_conf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/xpp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/yaml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/z80.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/zxbasic.php
Diffstat (limited to 'includes/db')
24 files changed, 3810 insertions, 3800 deletions
diff --git a/includes/db/ChronologyProtector.php b/includes/db/ChronologyProtector.php new file mode 100644 index 00000000..de5e72c3 --- /dev/null +++ b/includes/db/ChronologyProtector.php @@ -0,0 +1,106 @@ +<?php +/** + * Generator of database load balancing objects. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Database + */ + +/** + * Class for ensuring a consistent ordering of events as seen by the user, despite replication. + * Kind of like Hawking's [[Chronology Protection Agency]]. + */ +class ChronologyProtector { + /** @var Array (DB master name => position) */ + protected $startupPositions = array(); + /** @var Array (DB master name => position) */ + protected $shutdownPositions = array(); + + protected $initialized = false; // bool; whether the session data was loaded + + /** + * Initialise a LoadBalancer to give it appropriate chronology protection. + * + * If the session has a previous master position recorded, this will try to + * make sure that the next query to a slave of that master will see changes up + * to that position by delaying execution. The delay may timeout and allow stale + * data if no non-lagged slaves are available. + * + * @param $lb LoadBalancer + * @return void + */ + public function initLB( LoadBalancer $lb ) { + if ( $lb->getServerCount() <= 1 ) { + return; // non-replicated setup + } + if ( !$this->initialized ) { + $this->initialized = true; + if ( isset( $_SESSION[__CLASS__] ) && is_array( $_SESSION[__CLASS__] ) ) { + $this->startupPositions = $_SESSION[__CLASS__]; + } + } + $masterName = $lb->getServerName( 0 ); + if ( !empty( $this->startupPositions[$masterName] ) ) { + $info = $lb->parentInfo(); + $pos = $this->startupPositions[$masterName]; + wfDebug( __METHOD__ . ": LB " . $info['id'] . " waiting for master pos $pos\n" ); + $lb->waitFor( $pos ); + } + } + + /** + * Notify the ChronologyProtector that the LoadBalancer is about to shut + * down. Saves replication positions. + * + * @param $lb LoadBalancer + * @return void + */ + public function shutdownLB( LoadBalancer $lb ) { + if ( session_id() == '' || $lb->getServerCount() <= 1 ) { + return; // don't start a session; don't bother with non-replicated setups + } + $masterName = $lb->getServerName( 0 ); + if ( isset( $this->shutdownPositions[$masterName] ) ) { + return; // already done + } + // Only save the position if writes have been done on the connection + $db = $lb->getAnyOpenConnection( 0 ); + $info = $lb->parentInfo(); + if ( !$db || !$db->doneWrites() ) { + wfDebug( __METHOD__ . ": LB {$info['id']}, no writes done\n" ); + return; + } + $pos = $db->getMasterPos(); + wfDebug( __METHOD__ . ": LB {$info['id']} has master pos $pos\n" ); + $this->shutdownPositions[$masterName] = $pos; + } + + /** + * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now. + * May commit chronology data to persistent storage. + * + * @return void + */ + public function shutdown() { + if ( session_id() != '' && count( $this->shutdownPositions ) ) { + wfDebug( __METHOD__ . ": saving master pos for " . + count( $this->shutdownPositions ) . " master(s)\n" ); + $_SESSION[__CLASS__] = $this->shutdownPositions; + } + } +} diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 4e43642f..819925cb 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -60,9 +60,9 @@ class CloneDatabase { * Constructor * * @param $db DatabaseBase A database subclass - * @param $tablesToClone Array An array of tables to clone, unprefixed - * @param $newTablePrefix String Prefix to assign to the tables - * @param $oldTablePrefix String Prefix on current tables, if not $wgDBprefix + * @param array $tablesToClone An array of tables to clone, unprefixed + * @param string $newTablePrefix Prefix to assign to the tables + * @param string $oldTablePrefix Prefix on current tables, if not $wgDBprefix * @param $dropCurrentTables bool */ public function __construct( DatabaseBase $db, array $tablesToClone, @@ -77,7 +77,7 @@ class CloneDatabase { /** * Set whether to use temporary tables or not - * @param $u Bool Use temporary tables when cloning the structure + * @param bool $u Use temporary tables when cloning the structure */ public function useTemporaryTables( $u = true ) { $this->useTemporaryTables = $u; @@ -87,40 +87,37 @@ class CloneDatabase { * Clone the table structure */ public function cloneTableStructure() { - - foreach( $this->tablesToClone as $tbl ) { + foreach ( $this->tablesToClone as $tbl ) { # Clean up from previous aborted run. So that table escaping # works correctly across DB engines, we need to change the pre- # fix back and forth so tableName() works right. - + self::changePrefix( $this->oldTablePrefix ); $oldTableName = $this->db->tableName( $tbl, 'raw' ); - + self::changePrefix( $this->newTablePrefix ); $newTableName = $this->db->tableName( $tbl, 'raw' ); - - if( $this->dropCurrentTables && !in_array( $this->db->getType(), array( 'postgres', 'oracle' ) ) ) { + + if ( $this->dropCurrentTables && !in_array( $this->db->getType(), array( 'postgres', 'oracle' ) ) ) { $this->db->dropTable( $tbl, __METHOD__ ); - wfDebug( __METHOD__." dropping {$newTableName}\n", true); + wfDebug( __METHOD__ . " dropping {$newTableName}\n", true ); //Dropping the oldTable because the prefix was changed } # Create new table - wfDebug( __METHOD__." duplicating $oldTableName to $newTableName\n", true ); + wfDebug( __METHOD__ . " duplicating $oldTableName to $newTableName\n", true ); $this->db->duplicateTableStructure( $oldTableName, $newTableName, $this->useTemporaryTables ); - } - } /** * Change the prefix back to the original. - * @param $dropTables bool Optionally drop the tables we created + * @param bool $dropTables Optionally drop the tables we created */ public function destroy( $dropTables = false ) { - if( $dropTables ) { + if ( $dropTables ) { self::changePrefix( $this->newTablePrefix ); - foreach( $this->tablesToClone as $tbl ) { + foreach ( $this->tablesToClone as $tbl ) { $this->db->dropTable( $tbl ); } } @@ -130,7 +127,7 @@ class CloneDatabase { /** * Change the table prefix on all open DB connections/ * - * @param $prefix + * @param $prefix * @return void */ public static function changePrefix( $prefix ) { @@ -140,8 +137,8 @@ class CloneDatabase { } /** - * @param $lb LoadBalancer - * @param $prefix + * @param $lb LoadBalancer + * @param $prefix * @return void */ public static function changeLBPrefix( $lb, $prefix ) { @@ -149,8 +146,8 @@ class CloneDatabase { } /** - * @param $db DatabaseBase - * @param $prefix + * @param $db DatabaseBase + * @param $prefix * @return void */ public static function changeDBPrefix( $db, $prefix ) { diff --git a/includes/db/Database.php b/includes/db/Database.php index 5f10b97d..10645608 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -24,13 +24,6 @@ * @ingroup Database */ -/** Number of times to re-try an operation in case of deadlock */ -define( 'DEADLOCK_TRIES', 4 ); -/** Minimum time to wait before retry, in microseconds */ -define( 'DEADLOCK_DELAY_MIN', 500000 ); -/** Maximum time to wait before retry */ -define( 'DEADLOCK_DELAY_MAX', 1500000 ); - /** * Base interface for all DBMS-specific code. At a bare minimum, all of the * following must be implemented to support MediaWiki @@ -49,10 +42,10 @@ interface DatabaseType { /** * Open a connection to the database. Usually aborts on failure * - * @param $server String: database server host - * @param $user String: database user name - * @param $password String: database user password - * @param $dbName String: database name + * @param string $server database server host + * @param string $user database user name + * @param string $password database user password + * @param string $dbName database name * @return bool * @throws DBConnectionError */ @@ -62,9 +55,10 @@ interface DatabaseType { * 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. * * @param $res ResultWrapper|object as returned from DatabaseBase::query(), etc. - * @return Row object + * @return object|bool * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchObject( $res ); @@ -72,9 +66,10 @@ interface DatabaseType { /** * 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. * * @param $res ResultWrapper result object as returned from DatabaseBase::query(), etc. - * @return Row object + * @return array|bool * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchRow( $res ); @@ -112,8 +107,8 @@ interface DatabaseType { * The value inserted should be fetched from nextSequenceValue() * * Example: - * $id = $dbw->nextSequenceValue('page_page_id_seq'); - * $dbw->insert('page',array('page_id' => $id)); + * $id = $dbw->nextSequenceValue( 'page_page_id_seq' ); + * $dbw->insert( 'page', array( 'page_id' => $id ) ); * $id = $dbw->insertId(); * * @return int @@ -149,8 +144,8 @@ interface DatabaseType { * mysql_fetch_field() wrapper * Returns false if the field doesn't exist * - * @param $table string: table name - * @param $field string: field name + * @param string $table table name + * @param string $field field name * * @return Field */ @@ -158,12 +153,12 @@ interface DatabaseType { /** * Get information about an index into an object - * @param $table string: Table name - * @param $index string: Index name - * @param $fname string: Calling function name + * @param string $table Table name + * @param string $index Index name + * @param string $fname Calling function name * @return Mixed: Database-specific index description class or false if the index does not exist */ - function indexInfo( $table, $index, $fname = 'Database::indexInfo' ); + function indexInfo( $table, $index, $fname = __METHOD__ ); /** * Get the number of rows affected by the last write query @@ -176,7 +171,7 @@ interface DatabaseType { /** * Wrapper for addslashes() * - * @param $s string: to be slashed. + * @param string $s to be slashed. * @return string: slashed string. */ function strencode( $s ); @@ -189,7 +184,7 @@ interface DatabaseType { * * @return string: wikitext of a link to the server software's web site */ - static function getSoftwareLink(); + function getSoftwareLink(); /** * A string describing the current software version, like from @@ -210,10 +205,22 @@ interface DatabaseType { } /** + * Interface for classes that implement or wrap DatabaseBase + * @ingroup Database + */ +interface IDatabase {} + +/** * Database abstraction object * @ingroup Database */ -abstract class DatabaseBase implements DatabaseType { +abstract class DatabaseBase implements IDatabase, DatabaseType { + /** Number of times to re-try an operation in case of deadlock */ + const DEADLOCK_TRIES = 4; + /** Minimum time to wait before retry, in microseconds */ + const DEADLOCK_DELAY_MIN = 500000; + /** Maximum time to wait before retry */ + const DEADLOCK_DELAY_MAX = 1500000; # ------------------------------------------------------------------------------ # Variables @@ -228,14 +235,14 @@ abstract class DatabaseBase implements DatabaseType { protected $mConn = null; protected $mOpened = false; - /** - * @since 1.20 - * @var array of Closure - */ + /** @var callable[] */ protected $mTrxIdleCallbacks = array(); + /** @var callable[] */ + protected $mTrxPreCommitCallbacks = array(); protected $mTablePrefix; protected $mFlags; + protected $mForeign; protected $mTrxLevel = 0; protected $mErrorCount = 0; protected $mLBInfo = array(); @@ -249,6 +256,43 @@ abstract class DatabaseBase implements DatabaseType { protected $delimiter = ';'; + /** + * Remembers the function name given for starting the most recent transaction via begin(). + * Used to provide additional context for error reporting. + * + * @var String + * @see DatabaseBase::mTrxLevel + */ + private $mTrxFname = null; + + /** + * Record if possible write queries were done in the last transaction started + * + * @var Bool + * @see DatabaseBase::mTrxLevel + */ + private $mTrxDoneWrites = false; + + /** + * Record if the current transaction was started implicitly due to DBO_TRX being set. + * + * @var Bool + * @see DatabaseBase::mTrxLevel + */ + private $mTrxAutomatic = false; + + /** + * @since 1.21 + * @var file handle for upgrade + */ + protected $fileHandle = null; + + /** + * @since 1.22 + * @var Process cache of VIEWs names in the database + */ + protected $allViews = null; + # ------------------------------------------------------------------------------ # Accessors # ------------------------------------------------------------------------------ @@ -266,6 +310,13 @@ abstract class DatabaseBase implements DatabaseType { } /** + * @return string: command delimiter used by this database engine + */ + public function getDelimiter() { + return $this->delimiter; + } + + /** * Boolean, controls output of large amounts of debug information. * @param $debug bool|null * - true to enable debugging @@ -315,6 +366,8 @@ abstract class DatabaseBase implements DatabaseType { * code should use lastErrno() and lastError() to handle the * situation as appropriate. * + * Do not use this function outside of the Database classes. + * * @param $ignoreErrors bool|null * * @return bool The previous value of the flag. @@ -329,7 +382,7 @@ abstract class DatabaseBase implements DatabaseType { * Historically, transactions were allowed to be "nested". This is no * longer supported, so this function really only returns a boolean. * - * @param $level int An integer (0 or 1), or omitted to leave it unchanged. + * @param int $level An integer (0 or 1), or omitted to leave it unchanged. * @return int The previous value */ public function trxLevel( $level = null ) { @@ -338,7 +391,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Get/set the number of errors logged. Only useful when errors are ignored - * @param $count int The count to set, or omitted to leave it unchanged. + * @param int $count The count to set, or omitted to leave it unchanged. * @return int The error count */ public function errorCount( $count = null ) { @@ -347,7 +400,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Get/set the table prefix. - * @param $prefix string The table prefix to set, or omitted to leave it unchanged. + * @param string $prefix The table prefix to set, or omitted to leave it unchanged. * @return string The previous table prefix. */ public function tablePrefix( $prefix = null ) { @@ -355,10 +408,19 @@ abstract class DatabaseBase implements DatabaseType { } /** + * Set the filehandle to copy write statements to. + * + * @param $fh filehandle + */ + public function setFileHandle( $fh ) { + $this->fileHandle = $fh; + } + + /** * Get properties passed down from the server info array of the load * balancer. * - * @param $name string The entry of the info array to get, or null to get the + * @param string $name The entry of the info array to get, or null to get the * whole array * * @return LoadBalancer|null @@ -441,7 +503,7 @@ abstract class DatabaseBase implements DatabaseType { * Returns true if this database uses timestamps rather than integers * * @return bool - */ + */ public function realTimestamps() { return false; } @@ -504,12 +566,14 @@ abstract class DatabaseBase implements DatabaseType { /** * Returns true if there is a transaction open with possible write - * queries or transaction idle callbacks waiting on it to finish. + * queries or transaction pre-commit/idle callbacks waiting on it to finish. * * @return bool */ public function writesOrCallbacksPending() { - return $this->mTrxLevel && ( $this->mDoneWrites || $this->mTrxIdleCallbacks ); + return $this->mTrxLevel && ( + $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks + ); } /** @@ -526,7 +590,6 @@ abstract class DatabaseBase implements DatabaseType { * @param $flag Integer: DBO_* constants from Defines.php: * - DBO_DEBUG: output some debug info (same as debug()) * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) - * - DBO_IGNORE: ignore errors (same as ignoreErrors()) * - DBO_TRX: automatically start transactions * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode * and removes it in command line mode @@ -535,8 +598,8 @@ abstract class DatabaseBase implements DatabaseType { public function setFlag( $flag ) { global $wgDebugDBTransactions; $this->mFlags |= $flag; - if ( ( $flag & DBO_TRX) & $wgDebugDBTransactions ) { - wfDebug("Implicit transactions are now disabled.\n"); + if ( ( $flag & DBO_TRX ) & $wgDebugDBTransactions ) { + wfDebug( "Implicit transactions are now disabled.\n" ); } } @@ -549,7 +612,7 @@ abstract class DatabaseBase implements DatabaseType { global $wgDebugDBTransactions; $this->mFlags &= ~$flag; if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) { - wfDebug("Implicit transactions are now disabled.\n"); + wfDebug( "Implicit transactions are now disabled.\n" ); } } @@ -605,15 +668,28 @@ abstract class DatabaseBase implements DatabaseType { /** * Constructor. - * @param $server String: database server host - * @param $user String: database user name - * @param $password String: database user password - * @param $dbName String: database name + * + * FIXME: It is possible to construct a Database object with no associated + * connection object, by specifying no parameters to __construct(). This + * feature is deprecated and should be removed. + * + * FIXME: The long list of formal parameters here is not really appropriate + * for MySQL, and not at all appropriate for any other DBMS. It should be + * replaced by named parameters as in DatabaseBase::factory(). + * + * DatabaseBase subclasses should not be constructed directly in external + * code. DatabaseBase::factory() should be used instead. + * + * @param string $server database server host + * @param string $user database user name + * @param string $password database user password + * @param string $dbName database name * @param $flags - * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php + * @param string $tablePrefix database table prefixes. By default use the prefix gave in LocalSettings.php + * @param bool $foreign disable some operations specific to local databases */ function __construct( $server = false, $user = false, $password = false, $dbName = false, - $flags = 0, $tablePrefix = 'get from global' + $flags = 0, $tablePrefix = 'get from global', $foreign = false ) { global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions; @@ -623,12 +699,12 @@ abstract class DatabaseBase implements DatabaseType { if ( $wgCommandLineMode ) { $this->mFlags &= ~DBO_TRX; if ( $wgDebugDBTransactions ) { - wfDebug("Implicit transaction open disabled.\n"); + wfDebug( "Implicit transaction open disabled.\n" ); } } else { $this->mFlags |= DBO_TRX; if ( $wgDebugDBTransactions ) { - wfDebug("Implicit transaction open enabled.\n"); + wfDebug( "Implicit transaction open enabled.\n" ); } } } @@ -640,6 +716,8 @@ abstract class DatabaseBase implements DatabaseType { $this->mTablePrefix = $tablePrefix; } + $this->mForeign = $foreign; + if ( $user ) { $this->open( $server, $user, $password, $dbName ); } @@ -657,7 +735,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Given a DB type, construct the name of the appropriate child class of * DatabaseBase. This is designed to replace all of the manual stuff like: - * $class = 'Database' . ucfirst( strtolower( $type ) ); + * $class = 'Database' . ucfirst( strtolower( $dbType ) ); * as well as validate against the canonical list of DB types we have * * This factory function is mostly useful for when you need to connect to a @@ -665,32 +743,62 @@ abstract class DatabaseBase implements DatabaseType { * an extension, et cetera). Do not use this to connect to the MediaWiki * database. Example uses in core: * @see LoadBalancer::reallyOpenConnection() - * @see ExternalUser_MediaWiki::initFromCond() * @see ForeignDBRepo::getMasterDB() * @see WebInstaller_DBConnect::execute() * * @since 1.18 * - * @param $dbType String A possible DB type - * @param $p Array An array of options to pass to the constructor. - * Valid options are: host, user, password, dbname, flags, tablePrefix + * @param string $dbType A possible DB type + * @param array $p An array of options to pass to the constructor. + * Valid options are: host, user, password, dbname, flags, tablePrefix, driver * @return DatabaseBase subclass or null */ - public final static function factory( $dbType, $p = array() ) { + final public static function factory( $dbType, $p = array() ) { $canonicalDBTypes = array( - 'mysql', 'postgres', 'sqlite', 'oracle', 'mssql', 'ibm_db2' + 'mysql' => array( 'mysqli', 'mysql' ), + 'postgres' => array(), + 'sqlite' => array(), + 'oracle' => array(), + 'mssql' => array(), ); + + $driver = false; $dbType = strtolower( $dbType ); - $class = 'Database' . ucfirst( $dbType ); + if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) { + $possibleDrivers = $canonicalDBTypes[$dbType]; + if ( !empty( $p['driver'] ) ) { + if ( in_array( $p['driver'], $possibleDrivers ) ) { + $driver = $p['driver']; + } else { + throw new MWException( __METHOD__ . + " cannot construct Database with type '$dbType' and driver '{$p['driver']}'" ); + } + } else { + foreach ( $possibleDrivers as $posDriver ) { + if ( extension_loaded( $posDriver ) ) { + $driver = $posDriver; + break; + } + } + } + } else { + $driver = $dbType; + } + if ( $driver === false ) { + throw new MWException( __METHOD__ . + " no viable database extension found for type '$dbType'" ); + } - if( in_array( $dbType, $canonicalDBTypes ) || ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) ) { + $class = 'Database' . ucfirst( $driver ); + if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) { return new $class( isset( $p['host'] ) ? $p['host'] : false, isset( $p['user'] ) ? $p['user'] : false, isset( $p['password'] ) ? $p['password'] : false, isset( $p['dbname'] ) ? $p['dbname'] : false, isset( $p['flags'] ) ? $p['flags'] : 0, - isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global' + isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global', + isset( $p['foreign'] ) ? $p['foreign'] : false ); } else { return null; @@ -723,8 +831,9 @@ abstract class DatabaseBase implements DatabaseType { /** * @param $errno * @param $errstr + * @access private */ - protected function connectionErrorHandler( $errno, $errstr ) { + public function connectionErrorHandler( $errno, $errstr ) { $this->mPHPError = $errstr; } @@ -732,6 +841,7 @@ abstract class DatabaseBase implements DatabaseType { * Closes a database connection. * if it is open : commits any open transactions * + * @throws MWException * @return Bool operation success. true if already closed. */ public function close() { @@ -741,8 +851,14 @@ abstract class DatabaseBase implements DatabaseType { $this->mOpened = false; if ( $this->mConn ) { if ( $this->trxLevel() ) { - $this->commit( __METHOD__ ); + if ( !$this->mTrxAutomatic ) { + wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " . + " performing implicit commit before closing connection!" ); + } + + $this->commit( __METHOD__, 'flush' ); } + $ret = $this->closeConnection(); $this->mConn = false; return $ret; @@ -756,10 +872,11 @@ abstract class DatabaseBase implements DatabaseType { * @since 1.20 * @return bool: Whether connection was closed successfully */ - protected abstract function closeConnection(); + abstract protected function closeConnection(); /** - * @param $error String: fallback error message, used if none is given by DB + * @param string $error fallback error message, used if none is given by DB + * @throws DBConnectionError */ function reportConnectionError( $error = 'Unknown error' ) { $myError = $this->lastError(); @@ -777,7 +894,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $sql String: SQL query. * @return ResultWrapper Result object to feed to fetchObject, fetchRow, ...; or false on failure */ - protected abstract function doQuery( $sql ); + abstract protected function doQuery( $sql ); /** * Determine whether a query writes to the DB. @@ -809,27 +926,12 @@ abstract class DatabaseBase implements DatabaseType { * comment (you can use __METHOD__ or add some extra info) * @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors... * maybe best to catch the exception instead? + * @throws MWException * @return boolean|ResultWrapper. true for a successful write query, ResultWrapper object * for a successful read query, or false on failure if $tempIgnore set - * @throws DBQueryError Thrown when the database returns an error of any kind */ - public function query( $sql, $fname = '', $tempIgnore = false ) { - $isMaster = !is_null( $this->getLBInfo( 'master' ) ); - if ( !Profiler::instance()->isStub() ) { - # generalizeSQL will probably cut down the query to reasonable - # logging size most of the time. The substr is really just a sanity check. - - if ( $isMaster ) { - $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); - $totalProf = 'DatabaseBase::query-master'; - } else { - $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); - $totalProf = 'DatabaseBase::query'; - } - - wfProfileIn( $totalProf ); - wfProfileIn( $queryProf ); - } + public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) { + global $wgUser, $wgDebugDBTransactions; $this->mLastQuery = $sql; if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) { @@ -839,7 +941,6 @@ abstract class DatabaseBase implements DatabaseType { } # Add a comment for easy SHOW PROCESSLIST interpretation - global $wgUser; if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) { $userName = $wgUser->getName(); if ( mb_strlen( $userName ) > 15 ) { @@ -849,24 +950,49 @@ abstract class DatabaseBase implements DatabaseType { } else { $userName = ''; } - $commentedSql = preg_replace( '/\s/', " /* $fname $userName */ ", $sql, 1 ); + + // Add trace comment to the begin of the sql string, right after the operator. + // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598) + $commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 ); # If DBO_TRX is set, start a transaction - if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && - $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' ) { - # avoid establishing transactions for SHOW and SET statements too - + if ( ( $this->mFlags & DBO_TRX ) && !$this->mTrxLevel && + $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' ) + { + # Avoid establishing transactions for SHOW and SET statements too - # that would delay transaction initializations to once connection # is really used by application $sqlstart = substr( $sql, 0, 10 ); // very much worth it, benchmark certified(tm) if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) { - global $wgDebugDBTransactions; if ( $wgDebugDBTransactions ) { - wfDebug("Implicit transaction start.\n"); + wfDebug( "Implicit transaction start.\n" ); } $this->begin( __METHOD__ . " ($fname)" ); + $this->mTrxAutomatic = true; } } + # Keep track of whether the transaction has write queries pending + if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $this->isWriteQuery( $sql ) ) { + $this->mTrxDoneWrites = true; + Profiler::instance()->transactionWritingIn( $this->mServer, $this->mDBname ); + } + + $isMaster = !is_null( $this->getLBInfo( 'master' ) ); + if ( !Profiler::instance()->isStub() ) { + # generalizeSQL will probably cut down the query to reasonable + # logging size most of the time. The substr is really just a sanity check. + if ( $isMaster ) { + $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'DatabaseBase::query-master'; + } else { + $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'DatabaseBase::query'; + } + wfProfileIn( $totalProf ); + wfProfileIn( $queryProf ); + } + if ( $this->debug() ) { static $cnt = 0; @@ -878,10 +1004,6 @@ abstract class DatabaseBase implements DatabaseType { wfDebug( "Query {$this->mDBname} ($cnt) ($master): $sqlx\n" ); } - if ( istainted( $sql ) & TC_MYSQL ) { - throw new MWException( 'Tainted query found' ); - } - $queryId = MWDebug::query( $sql, $fname, $isMaster ); # Do the query and handle errors @@ -894,6 +1016,7 @@ abstract class DatabaseBase implements DatabaseType { # Transaction is gone, like it or not $this->mTrxLevel = 0; $this->mTrxIdleCallbacks = array(); // cancel + $this->mTrxPreCommitCallbacks = array(); // cancel wfDebug( "Connection lost, reconnecting...\n" ); if ( $this->ping() ) { @@ -933,6 +1056,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $sql String * @param $fname String * @param $tempIgnore Boolean + * @throws DBQueryError */ public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { # Ignore errors during error handling to avoid infinite recursion @@ -981,7 +1105,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Execute a prepared query with the various arguments - * @param $prepared String: the prepared sql + * @param string $prepared the prepared sql * @param $args Mixed: Either an array here, or put scalars as varargs * * @return ResultWrapper @@ -1001,8 +1125,8 @@ abstract class DatabaseBase implements DatabaseType { /** * For faking prepared SQL statements on DBs that don't support it directly. * - * @param $preparedQuery String: a 'preparable' SQL statement - * @param $args Array of arguments to fill it with + * @param string $preparedQuery a 'preparable' SQL statement + * @param array $args of arguments to fill it with * @return string executable SQL */ public function fillPrepared( $preparedQuery, $args ) { @@ -1019,20 +1143,26 @@ abstract class DatabaseBase implements DatabaseType { * while we're doing this. * * @param $matches Array + * @throws DBUnexpectedError * @return String */ protected function fillPreparedArg( $matches ) { - switch( $matches[1] ) { - case '\\?': return '?'; - case '\\!': return '!'; - case '\\&': return '&'; - } - - list( /* $n */ , $arg ) = each( $this->preparedArgs ); - - switch( $matches[1] ) { - case '?': return $this->addQuotes( $arg ); - case '!': return $arg; + switch ( $matches[1] ) { + case '\\?': + return '?'; + case '\\!': + return '!'; + case '\\&': + return '&'; + } + + list( /* $n */, $arg ) = each( $this->preparedArgs ); + + switch ( $matches[1] ) { + case '?': + return $this->addQuotes( $arg ); + case '!': + return $arg; case '&': # return $this->addQuotes( file_get_contents( $arg ) ); throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' ); @@ -1048,7 +1178,8 @@ abstract class DatabaseBase implements DatabaseType { * * @param $res Mixed: A SQL result */ - public function freeResult( $res ) {} + public function freeResult( $res ) { + } /** * A SELECT wrapper which returns a single field from a single result row. @@ -1058,18 +1189,18 @@ abstract class DatabaseBase implements DatabaseType { * * If no result rows are returned from the query, false is returned. * - * @param $table string|array Table name. See DatabaseBase::select() for details. - * @param $var string The field name to select. This must be a valid SQL + * @param string|array $table Table name. See DatabaseBase::select() for details. + * @param string $var The field name to select. This must be a valid SQL * fragment: do not use unvalidated user input. - * @param $cond string|array The condition array. See DatabaseBase::select() for details. - * @param $fname string The function name of the caller. - * @param $options string|array The query options. See DatabaseBase::select() for details. + * @param string|array $cond The condition array. See DatabaseBase::select() for details. + * @param string $fname The function name of the caller. + * @param string|array $options The query options. See DatabaseBase::select() for details. * * @return bool|mixed The value from the field, or false on failure. */ - public function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField', - $options = array() ) - { + public function selectField( $table, $var, $cond = '', $fname = __METHOD__, + $options = array() + ) { if ( !is_array( $options ) ) { $options = array( $options ); } @@ -1095,7 +1226,7 @@ abstract class DatabaseBase implements DatabaseType { * Returns an optional USE INDEX clause to go after the table, and a * string to go at the end of the query. * - * @param $options Array: associative array of options to be turned into + * @param array $options associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return Array * @see DatabaseBase::select() @@ -1112,26 +1243,9 @@ abstract class DatabaseBase implements DatabaseType { } } - if ( isset( $options['GROUP BY'] ) ) { - $gb = is_array( $options['GROUP BY'] ) - ? implode( ',', $options['GROUP BY'] ) - : $options['GROUP BY']; - $preLimitTail .= " GROUP BY {$gb}"; - } + $preLimitTail .= $this->makeGroupByWithHaving( $options ); - if ( isset( $options['HAVING'] ) ) { - $having = is_array( $options['HAVING'] ) - ? $this->makeList( $options['HAVING'], LIST_AND ) - : $options['HAVING']; - $preLimitTail .= " HAVING {$having}"; - } - - if ( isset( $options['ORDER BY'] ) ) { - $ob = is_array( $options['ORDER BY'] ) - ? implode( ',', $options['ORDER BY'] ) - : $options['ORDER BY']; - $preLimitTail .= " ORDER BY {$ob}"; - } + $preLimitTail .= $this->makeOrderBy( $options ); // if (isset($options['LIMIT'])) { // $tailOpts .= $this->limitResult('', $options['LIMIT'], @@ -1184,7 +1298,7 @@ abstract class DatabaseBase implements DatabaseType { $startOpts .= ' SQL_NO_CACHE'; } - if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { + if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) { $useIndex = $this->useIndexClause( $options['USE INDEX'] ); } else { $useIndex = ''; @@ -1194,14 +1308,57 @@ abstract class DatabaseBase implements DatabaseType { } /** + * Returns an optional GROUP BY with an optional HAVING + * + * @param array $options associative array of options + * @return string + * @see DatabaseBase::select() + * @since 1.21 + */ + public function makeGroupByWithHaving( $options ) { + $sql = ''; + if ( isset( $options['GROUP BY'] ) ) { + $gb = is_array( $options['GROUP BY'] ) + ? implode( ',', $options['GROUP BY'] ) + : $options['GROUP BY']; + $sql .= ' GROUP BY ' . $gb; + } + if ( isset( $options['HAVING'] ) ) { + $having = is_array( $options['HAVING'] ) + ? $this->makeList( $options['HAVING'], LIST_AND ) + : $options['HAVING']; + $sql .= ' HAVING ' . $having; + } + return $sql; + } + + /** + * Returns an optional ORDER BY + * + * @param array $options associative array of options + * @return string + * @see DatabaseBase::select() + * @since 1.21 + */ + public function makeOrderBy( $options ) { + if ( isset( $options['ORDER BY'] ) ) { + $ob = is_array( $options['ORDER BY'] ) + ? implode( ',', $options['ORDER BY'] ) + : $options['ORDER BY']; + return ' ORDER BY ' . $ob; + } + return ''; + } + + /** * Execute a SELECT query constructed using the various parameters provided. * See below for full details of the parameters. * - * @param $table String|Array Table name - * @param $vars String|Array Field names - * @param $conds String|Array Conditions - * @param $fname String Caller function name - * @param $options Array Query options + * @param string|array $table Table name + * @param string|array $vars Field names + * @param string|array $conds Conditions + * @param string $fname Caller function name + * @param array $options Query options * @param $join_conds Array Join conditions * * @param $table string|array @@ -1325,14 +1482,14 @@ abstract class DatabaseBase implements DatabaseType { * join, the second is an SQL fragment giving the join condition for that * table. For example: * - * array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * array( 'page' => array( 'LEFT JOIN', 'page_latest=rev_id' ) ) * * @return ResultWrapper. If the query returned no rows, a ResultWrapper * with no rows in it will be returned. If there was a query error, a * DBQueryError exception will be thrown, except if the "ignore errors" * option was set, in which case false will be returned. */ - public function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', + public function select( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) { $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); @@ -1345,17 +1502,17 @@ abstract class DatabaseBase implements DatabaseType { * doing UNION queries, where the SQL text of each query is needed. In general, * however, callers outside of Database classes should just use select(). * - * @param $table string|array Table name - * @param $vars string|array Field names - * @param $conds string|array Conditions - * @param $fname string Caller function name - * @param $options string|array Query options + * @param string|array $table Table name + * @param string|array $vars Field names + * @param string|array $conds Conditions + * @param string $fname Caller function name + * @param string|array $options Query options * @param $join_conds string|array Join conditions * * @return string SQL query string. * @see DatabaseBase::select() */ - public function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', + public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) { if ( is_array( $vars ) ) { @@ -1363,28 +1520,26 @@ abstract class DatabaseBase implements DatabaseType { } $options = (array)$options; + $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) ) + ? $options['USE INDEX'] + : array(); if ( is_array( $table ) ) { - $useIndex = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) ) - ? $options['USE INDEX'] - : array(); - if ( count( $join_conds ) || count( $useIndex ) ) { - $from = ' FROM ' . - $this->tableNamesWithUseIndexOrJOIN( $table, $useIndex, $join_conds ); - } else { - $from = ' FROM ' . implode( ',', $this->tableNamesWithAlias( $table ) ); - } + $from = ' FROM ' . + $this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds ); } elseif ( $table != '' ) { if ( $table[0] == ' ' ) { $from = ' FROM ' . $table; } else { - $from = ' FROM ' . $this->tableName( $table ); + $from = ' FROM ' . + $this->tableNamesWithUseIndexOrJOIN( array( $table ), $useIndexes, array() ); } } else { $from = ''; } - list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options ); + list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = + $this->makeSelectOptions( $options ); if ( !empty( $conds ) ) { if ( is_array( $conds ) ) { @@ -1413,16 +1568,16 @@ abstract class DatabaseBase implements DatabaseType { * that a single row object is returned. If the query returns no rows, * false is returned. * - * @param $table string|array Table name - * @param $vars string|array Field names - * @param $conds array Conditions - * @param $fname string Caller function name - * @param $options string|array Query options + * @param string|array $table Table name + * @param string|array $vars Field names + * @param array $conds Conditions + * @param string $fname Caller function name + * @param string|array $options Query options * @param $join_conds array|string Join conditions * * @return object|bool */ - public function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow', + public function selectRow( $table, $vars, $conds, $fname = __METHOD__, $options = array(), $join_conds = array() ) { $options = (array)$options; @@ -1455,15 +1610,15 @@ abstract class DatabaseBase implements DatabaseType { * * Takes the same arguments as DatabaseBase::select(). * - * @param $table String: table name - * @param Array|string $vars : unused - * @param Array|string $conds : filters on the table - * @param $fname String: function name for profiling - * @param $options Array: options for select + * @param string $table table name + * @param array|string $vars : unused + * @param array|string $conds : filters on the table + * @param string $fname function name for profiling + * @param array $options options for select * @return Integer: row count */ public function estimateRowCount( $table, $vars = '*', $conds = '', - $fname = 'DatabaseBase::estimateRowCount', $options = array() ) + $fname = __METHOD__, $options = array() ) { $rows = 0; $res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options ); @@ -1480,26 +1635,27 @@ abstract class DatabaseBase implements DatabaseType { * Removes most variables from an SQL query and replaces them with X or N for numbers. * It's only slightly flawed. Don't use for anything important. * - * @param $sql String A SQL Query + * @param string $sql A SQL Query * * @return string */ static function generalizeSQL( $sql ) { # This does the same as the regexp below would do, but in such a way # as to avoid crashing php on some large strings. - # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql); + # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql ); - $sql = str_replace ( "\\\\", '', $sql ); - $sql = str_replace ( "\\'", '', $sql ); - $sql = str_replace ( "\\\"", '', $sql ); - $sql = preg_replace ( "/'.*'/s", "'X'", $sql ); - $sql = preg_replace ( '/".*"/s', "'X'", $sql ); + $sql = str_replace( "\\\\", '', $sql ); + $sql = str_replace( "\\'", '', $sql ); + $sql = str_replace( "\\\"", '', $sql ); + $sql = preg_replace( "/'.*'/s", "'X'", $sql ); + $sql = preg_replace( '/".*"/s', "'X'", $sql ); # All newlines, tabs, etc replaced by single space - $sql = preg_replace ( '/\s+/', ' ', $sql ); + $sql = preg_replace( '/\s+/', ' ', $sql ); # All numbers => N - $sql = preg_replace ( '/-?[0-9]+/s', 'N', $sql ); + $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql ); + $sql = preg_replace( '/-?\d+/s', 'N', $sql ); return $sql; } @@ -1507,12 +1663,12 @@ abstract class DatabaseBase implements DatabaseType { /** * Determines whether a field exists in a table * - * @param $table String: table name - * @param $field String: filed to check on that table - * @param $fname String: calling function name (optional) + * @param string $table table name + * @param string $field filed to check on that table + * @param string $fname calling function name (optional) * @return Boolean: whether $table has filed $field */ - public function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) { + public function fieldExists( $table, $field, $fname = __METHOD__ ) { $info = $this->fieldInfo( $table, $field ); return (bool)$info; @@ -1529,7 +1685,11 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool|null */ - public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) { + public function indexExists( $table, $index, $fname = __METHOD__ ) { + if ( !$this->tableExists( $table ) ) { + return null; + } + $info = $this->indexInfo( $table, $index, $fname ); if ( is_null( $info ) ) { return null; @@ -1626,11 +1786,11 @@ abstract class DatabaseBase implements DatabaseType { * DatabaseBase::tableName(). * @param $a Array of rows to insert * @param $fname String Calling function name (use __METHOD__) for logs/profiling - * @param $options Array of options + * @param array $options of options * * @return bool */ - public function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) { + public function insert( $table, $a, $fname = __METHOD__, $options = array() ) { # No rows to insert, easy just return now if ( !count( $a ) ) { return true; @@ -1642,6 +1802,10 @@ abstract class DatabaseBase implements DatabaseType { $options = array( $options ); } + $fh = null; + if ( isset( $options['fileHandle'] ) ) { + $fh = $options['fileHandle']; + } $options = $this->makeInsertOptions( $options ); if ( isset( $a[0] ) && is_array( $a[0] ) ) { @@ -1669,13 +1833,19 @@ abstract class DatabaseBase implements DatabaseType { $sql .= '(' . $this->makeList( $a ) . ')'; } + if ( $fh !== null && false === fwrite( $fh, $sql ) ) { + return false; + } elseif ( $fh !== null ) { + return true; + } + return (bool)$this->query( $sql, $fname ); } /** * Make UPDATE options for the DatabaseBase::update function * - * @param $options Array: The options passed to DatabaseBase::update + * @param array $options The options passed to DatabaseBase::update * @return string */ protected function makeUpdateOptions( $options ) { @@ -1702,7 +1872,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $table String name of the table to UPDATE. This will be passed through * DatabaseBase::tableName(). * - * @param $values Array: An array of values to SET. For each array element, + * @param array $values An array of values to SET. For each array element, * the key gives the field name, and the value gives the data * to set that field to. The data will be quoted by * DatabaseBase::addQuotes(). @@ -1714,12 +1884,12 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: The function name of the caller (from __METHOD__), * for logging and profiling. * - * @param $options Array: An array of UPDATE options, can be: + * @param array $options An array of UPDATE options, can be: * - IGNORE: Ignore unique key conflicts * - LOW_PRIORITY: MySQL-specific, see MySQL manual. * @return Boolean */ - function update( $table, $values, $conds, $fname = 'DatabaseBase::update', $options = array() ) { + function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { $table = $this->tableName( $table ); $opts = $this->makeUpdateOptions( $options ); $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET ); @@ -1733,8 +1903,8 @@ abstract class DatabaseBase implements DatabaseType { /** * Makes an encoded list of strings from an array - * @param $a Array containing the data - * @param $mode int Constant + * @param array $a containing the data + * @param int $mode Constant * - LIST_COMMA: comma separated, no field names * - LIST_AND: ANDed WHERE clause (without the WHERE). See * the documentation for $conds in DatabaseBase::select(). @@ -1742,6 +1912,7 @@ abstract class DatabaseBase implements DatabaseType { * - LIST_SET: comma separated with field names, like a SET clause * - LIST_NAMES: comma separated field names * + * @throws MWException|DBUnexpectedError * @return string */ public function makeList( $a, $mode = LIST_COMMA ) { @@ -1771,7 +1942,7 @@ abstract class DatabaseBase implements DatabaseType { $list .= "$value"; } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { if ( count( $value ) == 0 ) { - throw new MWException( __METHOD__ . ': empty input' ); + throw new MWException( __METHOD__ . ": empty input for field $field" ); } elseif ( count( $value ) == 1 ) { // Special-case single values, as IN isn't terribly efficient // Don't necessarily assume the single key is 0; we don't @@ -1803,10 +1974,10 @@ abstract class DatabaseBase implements DatabaseType { * Build a partial where clause from a 2-d array such as used for LinkBatch. * The keys on each level may be either integers or strings. * - * @param $data Array: organized as 2-d + * @param array $data organized as 2-d * array(baseKeyVal => array(subKeyVal => [ignored], ...), ...) - * @param $baseKey String: field name to match the base-level keys to (eg 'pl_namespace') - * @param $subKey String: field name to match the sub-level keys to (eg 'pl_title') + * @param string $baseKey field name to match the base-level keys to (eg 'pl_namespace') + * @param string $subKey field name to match the sub-level keys to (eg 'pl_title') * @return Mixed: string SQL fragment, or false if no items in array. */ public function makeWhereFrom2d( $data, $baseKey, $subKey ) { @@ -1868,7 +2039,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Build a concatenation list to feed into a SQL query - * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting + * @param array $stringList list of raw SQL expressions; caller is responsible for any quoting * @return String */ public function buildConcat( $stringList ) { @@ -1916,8 +2087,8 @@ abstract class DatabaseBase implements DatabaseType { * themselves. Pass the canonical name to such functions. This is only needed * when calling query() directly. * - * @param $name String: database table name - * @param $format String One of: + * @param string $name database table name + * @param string $format One of: * quoted - Automatically pass the table name through addIdentifierQuotes() * so that it can be used in a query. * raw - Do not add identifier quotes to the table name @@ -1947,47 +2118,40 @@ abstract class DatabaseBase implements DatabaseType { # Split database and table into proper variables. # We reverse the explode so that database.table and table both output # the correct table. - $dbDetails = array_reverse( explode( '.', $name, 2 ) ); - if ( isset( $dbDetails[1] ) ) { - list( $table, $database ) = $dbDetails; + $dbDetails = explode( '.', $name, 2 ); + if ( count( $dbDetails ) == 2 ) { + list( $database, $table ) = $dbDetails; + # We don't want any prefix added in this case + $prefix = ''; } else { list( $table ) = $dbDetails; - } - $prefix = $this->mTablePrefix; # Default prefix - - # A database name has been specified in input. We don't want any - # prefixes added. - if ( isset( $database ) ) { - $prefix = ''; + if ( $wgSharedDB !== null # We have a shared database + && $this->mForeign == false # We're not working on a foreign database + && !$this->isQuotedIdentifier( $table ) # Paranoia check to prevent shared tables listing '`table`' + && in_array( $table, $wgSharedTables ) # A shared table is selected + ) { + $database = $wgSharedDB; + $prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix; + } else { + $database = null; + $prefix = $this->mTablePrefix; # Default prefix + } } - # Note that we use the long format because php will complain in in_array if - # the input is not an array, and will complain in is_array if it is not set. - if ( !isset( $database ) # Don't use shared database if pre selected. - && isset( $wgSharedDB ) # We have a shared database - && !$this->isQuotedIdentifier( $table ) # Paranoia check to prevent shared tables listing '`table`' - && isset( $wgSharedTables ) - && is_array( $wgSharedTables ) - && in_array( $table, $wgSharedTables ) ) { # A shared table is selected - $database = $wgSharedDB; - $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix; + # Quote $table and apply the prefix if not quoted. + $tableName = "{$prefix}{$table}"; + if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) ) { + $tableName = $this->addIdentifierQuotes( $tableName ); } - # Quote the $database and $table and apply the prefix if not quoted. - if ( isset( $database ) ) { + # Quote $database and merge it with the table name if needed + if ( $database !== null ) { if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) { $database = $this->addIdentifierQuotes( $database ); } + $tableName = $database . '.' . $tableName; } - $table = "{$prefix}{$table}"; - if ( $format == 'quoted' && !$this->isQuotedIdentifier( $table ) ) { - $table = $this->addIdentifierQuotes( "{$table}" ); - } - - # Merge our database and table into our final table name. - $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" ); - return $tableName; } @@ -1996,7 +2160,7 @@ abstract class DatabaseBase implements DatabaseType { * This is handy when you need to construct SQL for joins * * Example: - * extract($dbr->tableNames('user','watchlist')); + * extract( $dbr->tableNames( 'user', 'watchlist' ) ); * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user * WHERE wl_user=user_id AND wl_user=$nameWithQuotes"; * @@ -2018,7 +2182,7 @@ abstract class DatabaseBase implements DatabaseType { * This is handy when you need to construct SQL for joins * * Example: - * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist'); + * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' ); * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user * WHERE wl_user=user_id AND wl_user=$nameWithQuotes"; * @@ -2039,8 +2203,8 @@ abstract class DatabaseBase implements DatabaseType { * Get an aliased table name * e.g. tableName AS newTableName * - * @param $name string Table name, see tableName() - * @param $alias string|bool Alias (optional) + * @param string $name Table name, see tableName() + * @param string|bool $alias Alias (optional) * @return string SQL name for aliased table. Will not alias a table to its own name */ public function tableNameWithAlias( $name, $alias = false ) { @@ -2072,8 +2236,8 @@ abstract class DatabaseBase implements DatabaseType { * Get an aliased field name * e.g. fieldName AS newFieldName * - * @param $name string Field name - * @param $alias string|bool Alias (optional) + * @param string $name Field name + * @param string|bool $alias Alias (optional) * @return string SQL name for aliased field. Will not alias a field to its own name */ public function fieldNameWithAlias( $name, $alias = false ) { @@ -2105,7 +2269,7 @@ abstract class DatabaseBase implements DatabaseType { * Get the aliased table name clause for a FROM clause * which might have a JOIN and/or USE INDEX clause * - * @param $tables array ( [alias] => table ) + * @param array $tables ( [alias] => table ) * @param $use_index array Same as for select() * @param $join_conds array Same as for select() * @return string @@ -2155,11 +2319,11 @@ abstract class DatabaseBase implements DatabaseType { } // We can't separate explicit JOIN clauses with ',', use ' ' for those - $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : ""; - $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : ""; + $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : ""; + $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : ""; // Compile our final table clause - return implode( ' ', array( $straightJoins, $otherJoins ) ); + return implode( ' ', array( $implicitJoins, $explicitJoins ) ); } /** @@ -2172,9 +2336,9 @@ abstract class DatabaseBase implements DatabaseType { protected function indexName( $index ) { // Backwards-compatibility hack $renamed = array( - 'ar_usertext_timestamp' => 'usertext_timestamp', - 'un_user_id' => 'user_id', - 'un_user_ip' => 'user_ip', + 'ar_usertext_timestamp' => 'usertext_timestamp', + 'un_user_id' => 'user_id', + 'un_user_ip' => 'user_ip', ); if ( isset( $renamed[$index] ) ) { @@ -2185,8 +2349,7 @@ abstract class DatabaseBase implements DatabaseType { } /** - * If it's a string, adds quotes and backslashes - * Otherwise returns as-is + * Adds quotes and backslashes. * * @param $s string * @@ -2336,14 +2499,14 @@ abstract class DatabaseBase implements DatabaseType { * to collide. However if you do this, you run the risk of encountering * errors which wouldn't have occurred in MySQL. * - * @param $table String: The table to replace the row(s) in. - * @param $rows array Can be either a single row to insert, or multiple rows, + * @param string $table The table to replace the row(s) in. + * @param array $rows Can be either a single row to insert, or multiple rows, * in the same format as for DatabaseBase::insert() - * @param $uniqueIndexes array is an array of indexes. Each element may be either + * @param array $uniqueIndexes is an array of indexes. Each element may be either * a field name or an array of field names - * @param $fname String: Calling function name (use __METHOD__) for logs/profiling + * @param string $fname Calling function name (use __METHOD__) for logs/profiling */ - public function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) { + public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { $quotedTable = $this->tableName( $table ); if ( count( $rows ) == 0 ) { @@ -2355,7 +2518,7 @@ abstract class DatabaseBase implements DatabaseType { $rows = array( $rows ); } - foreach( $rows as $row ) { + foreach ( $rows as $row ) { # Delete rows which collide if ( $uniqueIndexes ) { $sql = "DELETE FROM $quotedTable WHERE "; @@ -2386,7 +2549,7 @@ abstract class DatabaseBase implements DatabaseType { } # Now insert the row - $this->insert( $table, $row ); + $this->insert( $table, $row, $fname ); } } @@ -2394,9 +2557,9 @@ abstract class DatabaseBase implements DatabaseType { * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE * statement. * - * @param $table string Table name - * @param $rows array Rows to insert - * @param $fname string Caller function name + * @param string $table Table name + * @param array $rows Rows to insert + * @param string $fname Caller function name * * @return ResultWrapper */ @@ -2425,6 +2588,92 @@ abstract class DatabaseBase implements DatabaseType { } /** + * INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table. + * + * This updates any conflicting rows (according to the unique indexes) using + * the provided SET clause and inserts any remaining (non-conflicted) rows. + * + * $rows may be either: + * - A single associative array. The array keys are the field names, and + * the values are the values to insert. The values are treated as data + * and will be quoted appropriately. If NULL is inserted, this will be + * converted to a database NULL. + * - An array with numeric keys, holding a list of associative arrays. + * This causes a multi-row INSERT on DBMSs that support it. The keys in + * each subarray must be identical to each other, and in the same order. + * + * It may be more efficient to leave off unique indexes which are unlikely + * to collide. However if you do this, you run the risk of encountering + * errors which wouldn't have occurred in MySQL. + * + * Usually throws a DBQueryError on failure. If errors are explicitly ignored, + * returns success. + * + * @param string $table Table name. This will be passed through DatabaseBase::tableName(). + * @param array $rows A single row or list of rows to insert + * @param array $uniqueIndexes List of single field names or field name tuples + * @param array $set An array of values to SET. For each array element, + * the key gives the field name, and the value gives the data + * to set that field to. The data will be quoted by + * DatabaseBase::addQuotes(). + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @param array $options of options + * + * @return bool + * @since 1.22 + */ + public function upsert( + $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ + ) { + if ( !count( $rows ) ) { + return true; // nothing to do + } + $rows = is_array( reset( $rows ) ) ? $rows : array( $rows ); + + if ( count( $uniqueIndexes ) ) { + $clauses = array(); // list WHERE clauses that each identify a single row + foreach ( $rows as $row ) { + foreach ( $uniqueIndexes as $index ) { + $index = is_array( $index ) ? $index : array( $index ); // columns + $rowKey = array(); // unique key to this row + foreach ( $index as $column ) { + $rowKey[$column] = $row[$column]; + } + $clauses[] = $this->makeList( $rowKey, LIST_AND ); + } + } + $where = array( $this->makeList( $clauses, LIST_OR ) ); + } else { + $where = false; + } + + $useTrx = !$this->mTrxLevel; + if ( $useTrx ) { + $this->begin( $fname ); + } + try { + # Update any existing conflicting row(s) + if ( $where !== false ) { + $ok = $this->update( $table, $set, $where, $fname ); + } else { + $ok = true; + } + # Now insert any non-conflicting row(s) + $ok = $this->insert( $table, $rows, $fname, array( 'IGNORE' ) ) && $ok; + } catch ( Exception $e ) { + if ( $useTrx ) { + $this->rollback( $fname ); + } + throw $e; + } + if ( $useTrx ) { + $this->commit( $fname ); + } + + return $ok; + } + + /** * DELETE where the condition is a join. * * MySQL overrides this to use a multi-table DELETE syntax, in other databases @@ -2443,9 +2692,10 @@ abstract class DatabaseBase implements DatabaseType { * ANDed together in the WHERE clause * @param $fname String: Calling function name (use __METHOD__) for * logs/profiling + * @throws DBUnexpectedError */ public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, - $fname = 'DatabaseBase::deleteJoin' ) + $fname = __METHOD__ ) { if ( !$conds ) { throw new DBUnexpectedError( $this, @@ -2503,14 +2753,15 @@ abstract class DatabaseBase implements DatabaseType { /** * DELETE query wrapper. * - * @param $table Array Table name - * @param $conds String|Array of conditions. See $conds in DatabaseBase::select() for + * @param array $table Table name + * @param string|array $conds of conditions. See $conds in DatabaseBase::select() for * the format. Use $conds == "*" to delete all rows - * @param $fname String name of the calling function + * @param string $fname name of the calling function * - * @return bool + * @throws DBUnexpectedError + * @return bool|ResultWrapper */ - public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) { + public function delete( $table, $conds, $fname = __METHOD__ ) { if ( !$conds ) { throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' ); } @@ -2519,7 +2770,10 @@ abstract class DatabaseBase implements DatabaseType { $sql = "DELETE FROM $table"; if ( $conds != '*' ) { - $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); + if ( is_array( $conds ) ) { + $conds = $this->makeList( $conds, LIST_AND ); + } + $sql .= ' WHERE ' . $conds; } return $this->query( $sql, $fname ); @@ -2529,30 +2783,30 @@ abstract class DatabaseBase implements DatabaseType { * INSERT SELECT wrapper. Takes data from a SELECT query and inserts it * into another table. * - * @param $destTable string The table name to insert into - * @param $srcTable string|array May be either a table name, or an array of table names + * @param string $destTable The table name to insert into + * @param string|array $srcTable May be either a table name, or an array of table names * to include in a join. * - * @param $varMap array must be an associative array of the form + * @param array $varMap must be an associative array of the form * array( 'dest1' => 'source1', ...). Source items may be literals * rather than field names, but strings should be quoted with * DatabaseBase::addQuotes() * - * @param $conds array Condition array. See $conds in DatabaseBase::select() for + * @param array $conds Condition array. See $conds in DatabaseBase::select() for * the details of the format of condition arrays. May be "*" to copy the * whole table. * - * @param $fname string The function name of the caller, from __METHOD__ + * @param string $fname The function name of the caller, from __METHOD__ * - * @param $insertOptions array Options for the INSERT part of the query, see + * @param array $insertOptions Options for the INSERT part of the query, see * DatabaseBase::insert() for details. - * @param $selectOptions array Options for the SELECT part of the query, see + * @param array $selectOptions Options for the SELECT part of the query, see * DatabaseBase::select() for details. * * @return ResultWrapper */ public function insertSelect( $destTable, $srcTable, $varMap, $conds, - $fname = 'DatabaseBase::insertSelect', + $fname = __METHOD__, $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); @@ -2568,7 +2822,7 @@ abstract class DatabaseBase implements DatabaseType { list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); if ( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); + $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); } else { $srcTable = $this->tableName( $srcTable ); } @@ -2602,10 +2856,11 @@ abstract class DatabaseBase implements DatabaseType { * The version provided by default works in MySQL and SQLite. It will very * likely need to be overridden for most other DBMSes. * - * @param $sql String SQL query we will append the limit too + * @param string $sql SQL query we will append the limit too * @param $limit Integer the SQL limit * @param $offset Integer|bool the SQL offset (default false) * + * @throws DBUnexpectedError * @return string */ public function limitResult( $sql, $limit, $offset = false ) { @@ -2630,7 +2885,7 @@ abstract class DatabaseBase implements DatabaseType { * Construct a UNION query * This is used for providing overload point for other DB abstractions * not compatible with the MySQL syntax. - * @param $sqls Array: SQL statements to combine + * @param array $sqls SQL statements to combine * @param $all Boolean: use UNION ALL * @return String: SQL fragment */ @@ -2643,9 +2898,9 @@ abstract class DatabaseBase implements DatabaseType { * Returns an SQL expression for a simple conditional. This doesn't need * to be overridden unless CASE isn't supported in your DBMS. * - * @param $cond string|array SQL expression which will result in a boolean value - * @param $trueVal String: SQL expression to return if true - * @param $falseVal String: SQL expression to return if false + * @param string|array $cond SQL expression which will result in a boolean value + * @param string $trueVal SQL expression to return if true + * @param string $falseVal SQL expression to return if false * @return String: SQL fragment */ public function conditional( $cond, $trueVal, $falseVal ) { @@ -2659,9 +2914,9 @@ abstract class DatabaseBase implements DatabaseType { * Returns a comand for str_replace function in SQL query. * Uses REPLACE() in MySQL * - * @param $orig String: column to modify - * @param $old String: column to seek - * @param $new String: column to replace with + * @param string $orig column to modify + * @param string $old column to seek + * @param string $new column to replace with * * @return string */ @@ -2743,7 +2998,7 @@ abstract class DatabaseBase implements DatabaseType { $args = func_get_args(); $function = array_shift( $args ); $oldIgnore = $this->ignoreErrors( true ); - $tries = DEADLOCK_TRIES; + $tries = self::DEADLOCK_TRIES; if ( is_array( $function ) ) { $fname = $function[0]; @@ -2760,7 +3015,7 @@ abstract class DatabaseBase implements DatabaseType { if ( $errno ) { if ( $this->wasDeadlock() ) { # Retry - usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) ); + usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) ); } else { $this->reportQueryError( $error, $errno, $sql, $fname ); } @@ -2850,23 +3105,47 @@ abstract class DatabaseBase implements DatabaseType { /** * Run an anonymous function as soon as there is no transaction pending. * If there is a transaction and it is rolled back, then the callback is cancelled. + * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls. * Callbacks must commit any transactions that they begin. * - * This is useful for updates to different systems or separate transactions are needed. + * This is useful for updates to different systems or when separate transactions are needed. + * For example, one might want to enqueue jobs into a system outside the database, but only + * after the database is updated so that the jobs will see the data when they actually run. + * It can also be used for updates that easily cause deadlocks if locks are held too long. * - * @param Closure $callback - * @return void + * @param callable $callback + * @since 1.20 */ - final public function onTransactionIdle( Closure $callback ) { + final public function onTransactionIdle( $callback ) { + $this->mTrxIdleCallbacks[] = array( $callback, wfGetCaller() ); + if ( !$this->mTrxLevel ) { + $this->runOnTransactionIdleCallbacks(); + } + } + + /** + * Run an anonymous function before the current transaction commits or now if there is none. + * If there is a transaction and it is rolled back, then the callback is cancelled. + * Callbacks must not start nor commit any transactions. + * + * This is useful for updates that easily cause deadlocks if locks are held too long + * but where atomicity is strongly desired for these updates and some related updates. + * + * @param callable $callback + * @since 1.22 + */ + final public function onTransactionPreCommitOrIdle( $callback ) { if ( $this->mTrxLevel ) { - $this->mTrxIdleCallbacks[] = $callback; + $this->mTrxPreCommitCallbacks[] = array( $callback, wfGetCaller() ); } else { - $callback(); + $this->onTransactionIdle( $callback ); // this will trigger immediately } } /** - * Actually run the "on transaction idle" callbacks + * Actually any "on transaction idle" callbacks. + * + * @since 1.20 */ protected function runOnTransactionIdleCallbacks() { $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled? @@ -2877,8 +3156,9 @@ abstract class DatabaseBase implements DatabaseType { $this->mTrxIdleCallbacks = array(); // recursion guard foreach ( $callbacks as $callback ) { try { + list( $phpCallback ) = $callback; $this->clearFlag( DBO_TRX ); // make each query its own transaction - $callback(); + call_user_func( $phpCallback ); $this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() } catch ( Exception $e ) {} } @@ -2890,19 +3170,78 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Begin a transaction + * Actually any "on transaction pre-commit" callbacks. + * + * @since 1.22 + */ + protected function runOnTransactionPreCommitCallbacks() { + $e = null; // last exception + do { // callbacks may add callbacks :) + $callbacks = $this->mTrxPreCommitCallbacks; + $this->mTrxPreCommitCallbacks = array(); // recursion guard + foreach ( $callbacks as $callback ) { + try { + list( $phpCallback ) = $callback; + call_user_func( $phpCallback ); + } catch ( Exception $e ) {} + } + } while ( count( $this->mTrxPreCommitCallbacks ) ); + + if ( $e instanceof Exception ) { + throw $e; // re-throw any last exception + } + } + + /** + * Begin a transaction. If a transaction is already in progress, that transaction will be committed before the + * new transaction is started. + * + * Note that when the DBO_TRX flag is set (which is usually the case for web requests, but not for maintenance scripts), + * any previous database query will have started a transaction automatically. + * + * Nesting of transactions is not supported. Attempts to nest transactions will cause a warning, unless the current + * transaction was started automatically because of the DBO_TRX flag. * * @param $fname string */ - final public function begin( $fname = 'DatabaseBase::begin' ) { + final public function begin( $fname = __METHOD__ ) { + global $wgDebugDBTransactions; + if ( $this->mTrxLevel ) { // implicit commit + if ( !$this->mTrxAutomatic ) { + // We want to warn about inadvertently nested begin/commit pairs, but not about + // auto-committing implicit transactions that were started by query() via DBO_TRX + $msg = "$fname: Transaction already in progress (from {$this->mTrxFname}), " . + " performing implicit commit!"; + wfWarn( $msg ); + wfLogDBError( $msg ); + } else { + // if the transaction was automatic and has done write operations, + // log it if $wgDebugDBTransactions is enabled. + if ( $this->mTrxDoneWrites && $wgDebugDBTransactions ) { + wfDebug( "$fname: Automatic transaction with writes in progress" . + " (from {$this->mTrxFname}), performing implicit commit!\n" + ); + } + } + + $this->runOnTransactionPreCommitCallbacks(); $this->doCommit( $fname ); + if ( $this->mTrxDoneWrites ) { + Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname ); + } $this->runOnTransactionIdleCallbacks(); } + $this->doBegin( $fname ); + $this->mTrxFname = $fname; + $this->mTrxDoneWrites = false; + $this->mTrxAutomatic = false; } /** + * Issues the BEGIN command to the database server. + * * @see DatabaseBase::begin() * @param type $fname */ @@ -2912,16 +3251,44 @@ abstract class DatabaseBase implements DatabaseType { } /** - * End a transaction + * Commits a transaction previously started using begin(). + * If no transaction is in progress, a warning is issued. + * + * Nesting of transactions is not supported. * * @param $fname string - */ - final public function commit( $fname = 'DatabaseBase::commit' ) { + * @param string $flush Flush flag, set to 'flush' to disable warnings about explicitly committing implicit + * transactions, or calling commit when no transaction is in progress. + * This will silently break any ongoing explicit transaction. Only set the flush flag if you are sure + * that it is safe to ignore these warnings in your context. + */ + final public function commit( $fname = __METHOD__, $flush = '' ) { + if ( $flush != 'flush' ) { + if ( !$this->mTrxLevel ) { + wfWarn( "$fname: No transaction to commit, something got out of sync!" ); + } elseif ( $this->mTrxAutomatic ) { + wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" ); + } + } else { + if ( !$this->mTrxLevel ) { + return; // nothing to do + } elseif ( !$this->mTrxAutomatic ) { + wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" ); + } + } + + $this->runOnTransactionPreCommitCallbacks(); $this->doCommit( $fname ); + if ( $this->mTrxDoneWrites ) { + Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname ); + } + $this->mTrxDoneWrites = false; $this->runOnTransactionIdleCallbacks(); } /** + * Issues the COMMIT command to the database server. + * * @see DatabaseBase::commit() * @param type $fname */ @@ -2933,17 +3300,29 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Rollback a transaction. + * Rollback a transaction previously started using begin(). + * If no transaction is in progress, a warning is issued. + * * No-op on non-transactional databases. * * @param $fname string */ - final public function rollback( $fname = 'DatabaseBase::rollback' ) { + final public function rollback( $fname = __METHOD__ ) { + if ( !$this->mTrxLevel ) { + wfWarn( "$fname: No transaction to rollback, something got out of sync!" ); + } $this->doRollback( $fname ); $this->mTrxIdleCallbacks = array(); // cancel + $this->mTrxPreCommitCallbacks = array(); // cancel + if ( $this->mTrxDoneWrites ) { + Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname ); + } + $this->mTrxDoneWrites = false; } /** + * Issues the ROLLBACK command to the database server. + * * @see DatabaseBase::rollback() * @param type $fname */ @@ -2962,15 +3341,16 @@ abstract class DatabaseBase implements DatabaseType { * The table names passed to this function shall not be quoted (this * function calls addIdentifierQuotes when needed). * - * @param $oldName String: name of table whose structure should be copied - * @param $newName String: name of table to be created + * @param string $oldName name of table whose structure should be copied + * @param string $newName name of table to be created * @param $temporary Boolean: whether the new table should be temporary - * @param $fname String: calling function name + * @param string $fname calling function name + * @throws MWException * @return Boolean: true if operation was successful */ public function duplicateTableStructure( $oldName, $newName, $temporary = false, - $fname = 'DatabaseBase::duplicateTableStructure' ) - { + $fname = __METHOD__ + ) { throw new MWException( 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' ); } @@ -2978,14 +3358,49 @@ abstract class DatabaseBase implements DatabaseType { /** * List all tables on the database * - * @param $prefix string Only show tables with this prefix, e.g. mw_ - * @param $fname String: calling function name + * @param string $prefix Only show tables with this prefix, e.g. mw_ + * @param string $fname calling function name + * @throws MWException */ - function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) { + function listTables( $prefix = null, $fname = __METHOD__ ) { throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' ); } /** + * Reset the views process cache set by listViews() + * @since 1.22 + */ + final public function clearViewsCache() { + $this->allViews = null; + } + + /** + * Lists all the VIEWs in the database + * + * For caching purposes the list of all views should be stored in + * $this->allViews. The process cache can be cleared with clearViewsCache() + * + * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_ + * @param string $fname Name of calling function + * @throws MWException + * @since 1.22 + */ + public function listViews( $prefix = null, $fname = __METHOD__ ) { + throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' ); + } + + /** + * Differentiates between a TABLE and a VIEW + * + * @param $name string: Name of the database-structure to test. + * @throws MWException + * @since 1.22 + */ + public function isView( $name ) { + throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' ); + } + + /** * Convert a timestamp in one of the formats accepted by wfTimestamp() * to the format used for inserting into timestamp fields in this DBMS. * @@ -3114,7 +3529,8 @@ abstract class DatabaseBase implements DatabaseType { * @param $options Array * @return void */ - public function setSessionOptions( array $options ) {} + public function setSessionOptions( array $options ) { + } /** * Read and execute SQL commands from a file. @@ -3122,15 +3538,18 @@ abstract class DatabaseBase implements DatabaseType { * Returns true on success, error string or exception on failure (depending * on object's error ignore settings). * - * @param $filename String: File name to open - * @param $lineCallback Callback: Optional function called before reading each line - * @param $resultCallback Callback: Optional function called for each MySQL result - * @param $fname String: Calling function name or false if name should be + * @param string $filename File name to open + * @param bool|callable $lineCallback Optional function called before reading each line + * @param bool|callable $resultCallback Optional function called for each MySQL result + * @param bool|string $fname Calling function name or false if name should be * generated dynamically using $filename + * @param bool|callable $inputCallback Callback: Optional function called for each complete line sent + * @throws MWException + * @throws Exception|MWException * @return bool|string */ public function sourceFile( - $filename, $lineCallback = false, $resultCallback = false, $fname = false + $filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false ) { wfSuppressWarnings(); $fp = fopen( $filename, 'r' ); @@ -3145,7 +3564,7 @@ abstract class DatabaseBase implements DatabaseType { } try { - $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname ); + $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback ); } catch ( MWException $e ) { fclose( $fp ); @@ -3162,7 +3581,7 @@ abstract class DatabaseBase implements DatabaseType { * from updaters.inc. Keep in mind this always returns a patch, as * it fails back to MySQL if no DB-specific patch can be found * - * @param $patch String The name of the patch, like patch-something.sql + * @param string $patch The name of the patch, like patch-something.sql * @return String Full path to patch file */ public function patchPath( $patch ) { @@ -3181,7 +3600,7 @@ abstract class DatabaseBase implements DatabaseType { * ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at * all. If it's set to false, $GLOBALS will be used. * - * @param $vars bool|array mapping variable name to value. + * @param bool|array $vars mapping variable name to value. */ public function setSchemaVars( $vars ) { $this->mSchemaVars = $vars; @@ -3194,14 +3613,14 @@ abstract class DatabaseBase implements DatabaseType { * on object's error ignore settings). * * @param $fp Resource: File handle - * @param $lineCallback Callback: Optional function called before reading each line + * @param $lineCallback Callback: Optional function called before reading each query * @param $resultCallback Callback: Optional function called for each MySQL result - * @param $fname String: Calling function name - * @param $inputCallback Callback: Optional function called for each complete line (ended with ;) sent + * @param string $fname Calling function name + * @param $inputCallback Callback: Optional function called for each complete query sent * @return bool|string */ public function sourceStream( $fp, $lineCallback = false, $resultCallback = false, - $fname = 'DatabaseBase::sourceStream', $inputCallback = false ) + $fname = __METHOD__, $inputCallback = false ) { $cmd = ''; @@ -3230,20 +3649,19 @@ abstract class DatabaseBase implements DatabaseType { if ( $done || feof( $fp ) ) { $cmd = $this->replaceVars( $cmd ); - if ( $inputCallback ) { - call_user_func( $inputCallback, $cmd ); - } - $res = $this->query( $cmd, $fname ); - if ( $resultCallback ) { - call_user_func( $resultCallback, $res, $this ); - } + if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) { + $res = $this->query( $cmd, $fname ); - if ( false === $res ) { - $err = $this->lastError(); - return "Query \"{$cmd}\" failed with error code \"$err\".\n"; - } + if ( $resultCallback ) { + call_user_func( $resultCallback, $res, $this ); + } + if ( false === $res ) { + $err = $this->lastError(); + return "Query \"{$cmd}\" failed with error code \"$err\".\n"; + } + } $cmd = ''; } } @@ -3254,8 +3672,8 @@ abstract class DatabaseBase implements DatabaseType { /** * Called by sourceStream() to check if we've reached a statement end * - * @param $sql String SQL assembled so far - * @param $newLine String New line about to be added to $sql + * @param string $sql SQL assembled so far + * @param string $newLine New line about to be added to $sql * @return Bool Whether $newLine contains end of the statement */ public function streamStatementEnd( &$sql, &$newLine ) { @@ -3283,7 +3701,7 @@ abstract class DatabaseBase implements DatabaseType { * - / *$var* / is just encoded, besides traditional table prefix and * table options its use should be avoided. * - * @param $ins String: SQL statement to replace variables in + * @param string $ins SQL statement to replace variables in * @return String The new SQL statement with variables replaced */ protected function replaceSchemaVars( $ins ) { @@ -3294,7 +3712,7 @@ abstract class DatabaseBase implements DatabaseType { // replace `{$var}` $ins = str_replace( '`{$' . $var . '}`', $this->addIdentifierQuotes( $value ), $ins ); // replace /*$var*/ - $ins = str_replace( '/*$' . $var . '*/', $this->strencode( $value ) , $ins ); + $ins = str_replace( '/*$' . $var . '*/', $this->strencode( $value ), $ins ); } return $ins; } @@ -3371,8 +3789,8 @@ abstract class DatabaseBase implements DatabaseType { /** * Check to see if a named lock is available. This is non-blocking. * - * @param $lockName String: name of lock to poll - * @param $method String: name of method calling us + * @param string $lockName name of lock to poll + * @param string $method name of method calling us * @return Boolean * @since 1.20 */ @@ -3386,8 +3804,8 @@ abstract class DatabaseBase implements DatabaseType { * Abstracted from Filestore::lock() so child classes can implement for * their own needs. * - * @param $lockName String: name of lock to aquire - * @param $method String: name of method calling us + * @param string $lockName name of lock to aquire + * @param string $method name of method calling us * @param $timeout Integer: timeout * @return Boolean */ @@ -3398,8 +3816,8 @@ abstract class DatabaseBase implements DatabaseType { /** * Release a lock. * - * @param $lockName String: Name of lock to release - * @param $method String: Name of method calling us + * @param string $lockName Name of lock to release + * @param string $method Name of method calling us * * @return int Returns 1 if the lock was released, 0 if the lock was not established * by this thread (in which case the lock is not released), and NULL if the named @@ -3412,10 +3830,10 @@ abstract class DatabaseBase implements DatabaseType { /** * Lock specific tables * - * @param $read Array of tables to lock for read access - * @param $write Array of tables to lock for write access - * @param $method String name of caller - * @param $lowPriority bool Whether to indicate writes to be LOW PRIORITY + * @param array $read of tables to lock for read access + * @param array $write of tables to lock for write access + * @param string $method name of caller + * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY * * @return bool */ @@ -3426,7 +3844,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Unlock specific tables * - * @param $method String the caller + * @param string $method the caller * * @return bool */ @@ -3441,12 +3859,12 @@ abstract class DatabaseBase implements DatabaseType { * @return bool|ResultWrapper * @since 1.18 */ - public function dropTable( $tableName, $fName = 'DatabaseBase::dropTable' ) { - if( !$this->tableExists( $tableName, $fName ) ) { + public function dropTable( $tableName, $fName = __METHOD__ ) { + if ( !$this->tableExists( $tableName, $fName ) ) { return false; } $sql = "DROP TABLE " . $this->tableName( $tableName ); - if( $this->cascadingDeletes() ) { + if ( $this->cascadingDeletes() ) { $sql .= " CASCADE"; } return $this->query( $sql, $fName ); @@ -3476,7 +3894,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Encode an expiry time into the DBMS dependent format * - * @param $expiry String: timestamp for expiry, or the 'infinity' string + * @param string $expiry timestamp for expiry, or the 'infinity' string * @return String */ public function encodeExpiry( $expiry ) { @@ -3488,7 +3906,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Decode an expiry time into a DBMS independent format * - * @param $expiry String: DB timestamp field value for expiry + * @param string $expiry DB timestamp field value for expiry * @param $format integer: TS_* constant, defaults to TS_MW * @return String */ @@ -3518,9 +3936,21 @@ abstract class DatabaseBase implements DatabaseType { return (string)$this->mConn; } + /** + * Run a few simple sanity checks + */ public function __destruct() { - if ( count( $this->mTrxIdleCallbacks ) ) { // sanity - trigger_error( "Transaction idle callbacks still pending." ); + if ( $this->mTrxLevel && $this->mTrxDoneWrites ) { + trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." ); + } + if ( count( $this->mTrxIdleCallbacks ) || count( $this->mTrxPreCommitCallbacks ) ) { + $callers = array(); + foreach ( $this->mTrxIdleCallbacks as $callbackInfo ) { + $callers[] = $callbackInfo[1]; + + } + $callers = implode( ', ', $callers ); + trigger_error( "DB transaction callbacks still pending (from $callers)." ); } } } diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php index a53a6747..0875695f 100644 --- a/includes/db/DatabaseError.php +++ b/includes/db/DatabaseError.php @@ -35,32 +35,20 @@ class DBError extends MWException { /** * Construct a database error * @param $db DatabaseBase object which threw the error - * @param $error String A simple error message to be used for debugging + * @param string $error A simple error message to be used for debugging */ - function __construct( DatabaseBase &$db, $error ) { + function __construct( DatabaseBase $db = null, $error ) { $this->db = $db; parent::__construct( $error ); } /** - * @param $html string - * @return string - */ - protected function getContentMessage( $html ) { - if ( $html ) { - return nl2br( htmlspecialchars( $this->getMessage() ) ); - } else { - return $this->getMessage(); - } - } - - /** * @return string */ function getText() { global $wgShowDBErrorBacktrace; - $s = $this->getContentMessage( false ) . "\n"; + $s = $this->getTextContent() . "\n"; if ( $wgShowDBErrorBacktrace ) { $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; @@ -75,14 +63,29 @@ class DBError extends MWException { function getHTML() { global $wgShowDBErrorBacktrace; - $s = $this->getContentMessage( true ); + $s = $this->getHTMLContent(); if ( $wgShowDBErrorBacktrace ) { - $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); + $s .= '<p>Backtrace:</p><p>' . + nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . '</p>'; } return $s; } + + /** + * @return string + */ + protected function getTextContent() { + return $this->getMessage(); + } + + /** + * @return string + */ + protected function getHTMLContent() { + return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) . '</p>'; + } } /** @@ -91,16 +94,17 @@ class DBError extends MWException { class DBConnectionError extends DBError { public $error; - function __construct( DatabaseBase &$db, $error = 'unknown error' ) { + function __construct( DatabaseBase $db = null, $error = 'unknown error' ) { $msg = 'DB connection error'; if ( trim( $error ) != '' ) { $msg .= ": $error"; + } elseif ( $db ) { + $error = $this->db->getServer(); } - $this->error = $error; - parent::__construct( $db, $msg ); + $this->error = $error; } /** @@ -130,10 +134,10 @@ class DBConnectionError extends DBError { } /** - * @return bool + * @return boolean */ - function getLogMessage() { - # Don't send to the exception log + function isLoggable() { + // Don't send to the exception log, already in dberror log return false; } @@ -141,42 +145,54 @@ class DBConnectionError extends DBError { * @return string */ function getPageTitle() { - global $wgSitename; - return htmlspecialchars( $this->msg( 'dberr-header', "$wgSitename has a problem" ) ); + return $this->msg( 'dberr-header', 'This wiki has a problem' ); } /** * @return string */ function getHTML() { - global $wgShowDBErrorBacktrace; + global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors; - $sorry = htmlspecialchars( $this->msg( 'dberr-problems', 'Sorry! This site is experiencing technical difficulties.' ) ); + $sorry = htmlspecialchars( $this->msg( 'dberr-problems', "Sorry!\nThis site is experiencing technical difficulties." ) ); $again = htmlspecialchars( $this->msg( 'dberr-again', 'Try waiting a few minutes and reloading.' ) ); - $info = htmlspecialchars( $this->msg( 'dberr-info', '(Can\'t contact the database server: $1)' ) ); - - # No database access - MessageCache::singleton()->disable(); - if ( trim( $this->error ) == '' ) { - $this->error = $this->db->getProperty( 'mServer' ); + if ( $wgShowHostnames || $wgShowSQLErrors ) { + $info = str_replace( + '$1', Html::element( 'span', array( 'dir' => 'ltr' ), $this->error ), + htmlspecialchars( $this->msg( 'dberr-info', '(Cannot contact the database server: $1)' ) ) + ); + } else { + $info = htmlspecialchars( $this->msg( 'dberr-info-hidden', '(Cannot contact the database server)' ) ); } - $this->error = Html::element( 'span', array( 'dir' => 'ltr' ), $this->error ); + # No database access + MessageCache::singleton()->disable(); - $noconnect = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>"; - $text = str_replace( '$1', $this->error, $noconnect ); + $text = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>"; if ( $wgShowDBErrorBacktrace ) { - $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); + $text .= '<p>Backtrace:</p><p>' . + nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . '</p>'; } - $extra = $this->searchForm(); + $text .= '<hr />'; + $text .= $this->searchForm(); - return "$text<hr />$extra"; + return $text; + } + + protected function getTextContent() { + global $wgShowHostnames, $wgShowSQLErrors; + + if ( $wgShowHostnames || $wgShowSQLErrors ) { + return $this->getMessage(); + } else { + return 'DB connection error'; + } } - public function reportHTML(){ + public function reportHTML() { global $wgUseFileCache; # Check whether we can serve a file-cached copy of the page with the error underneath @@ -188,7 +204,7 @@ class DBConnectionError extends DBError { # Hack: extend the body for error messages $cache = str_replace( array( '</html>', '</body>' ), '', $cache ); # Add cache notice... - $cache .= '<div style="color:red;font-size:150%;font-weight:bold;">'. + $cache .= '<div style="color:red;font-size:150%;font-weight:bold;">' . htmlspecialchars( $this->msg( 'dberr-cachederror', 'This is a cached copy of the requested page, and may not be up to date. ' ) ) . '</div>'; @@ -288,11 +304,11 @@ class DBQueryError extends DBError { * @param $sql string * @param $fname string */ - function __construct( DatabaseBase &$db, $error, $errno, $sql, $fname ) { - $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . - "Query: $sql\n" . - "Function: $fname\n" . - "Error: $errno $error\n"; + function __construct( DatabaseBase $db, $error, $errno, $sql, $fname ) { + $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; parent::__construct( $db, $message ); $this->error = $error; @@ -302,55 +318,107 @@ class DBQueryError extends DBError { } /** - * @param $html string + * @return boolean + */ + function isLoggable() { + // Don't send to the exception log, already in dberror log + return false; + } + + /** * @return string */ - function getContentMessage( $html ) { - if ( $this->useMessageCache() ) { - if ( $html ) { - $msg = 'dberrortext'; - $sql = htmlspecialchars( $this->getSQL() ); - $fname = htmlspecialchars( $this->fname ); - $error = htmlspecialchars( $this->error ); - } else { - $msg = 'dberrortextcl'; - $sql = $this->getSQL(); - $fname = $this->fname; - $error = $this->error; + function getPageTitle() { + return $this->msg( 'databaseerror', 'Database error' ); + } + + /** + * @return string + */ + protected function getHTMLContent() { + $key = 'databaseerror-text'; + $s = Html::element( 'p', array(), $this->msg( $key, $this->getFallbackMessage( $key ) ) ); + + $details = $this->getTechnicalDetails(); + if ( $details ) { + $s .= '<ul>'; + foreach ( $details as $key => $detail ) { + $s .= str_replace( + '$1', call_user_func_array( 'Html::element', $detail ), + Html::element( 'li', array(), + $this->msg( $key, $this->getFallbackMessage( $key ) ) + ) + ); } - return wfMessage( $msg )->rawParams( $sql, $fname, $this->errno, $error )->text(); - } else { - return parent::getContentMessage( $html ); + $s .= '</ul>'; } + + return $s; } /** - * @return String + * @return string */ - function getSQL() { - global $wgShowSQLErrors; + protected function getTextContent() { + $key = 'databaseerror-textcl'; + $s = $this->msg( $key, $this->getFallbackMessage( $key ) ) . "\n"; - if ( !$wgShowSQLErrors ) { - return $this->msg( 'sqlhidden', 'SQL hidden' ); - } else { - return $this->sql; + foreach ( $this->getTechnicalDetails() as $key => $detail ) { + $s .= $this->msg( $key, $this->getFallbackMessage( $key ), $detail[2] ) . "\n"; } + + return $s; } /** - * @return bool + * Make a list of technical details that can be shown to the user. This information can + * aid in debugging yet may be useful to an attacker trying to exploit a security weakness + * in the software or server configuration. + * + * Thus no such details are shown by default, though if $wgShowHostnames is true, only the + * full SQL query is hidden; in fact, the error message often does contain a hostname, and + * sites using this option probably don't care much about "security by obscurity". Of course, + * if $wgShowSQLErrors is true, the SQL query *is* shown. + * + * @return array: Keys are message keys; values are arrays of arguments for Html::element(). + * Array will be empty if users are not allowed to see any of these details at all. */ - function getLogMessage() { - # Don't send to the exception log - return false; + protected function getTechnicalDetails() { + global $wgShowHostnames, $wgShowSQLErrors; + + $attribs = array( 'dir' => 'ltr' ); + $details = array(); + + if ( $wgShowSQLErrors ) { + $details['databaseerror-query'] = array( + 'div', array( 'class' => 'mw-code' ) + $attribs, $this->sql ); + } + + if ( $wgShowHostnames || $wgShowSQLErrors ) { + $errorMessage = $this->errno . ' ' . $this->error; + $details['databaseerror-function'] = array( 'code', $attribs, $this->fname ); + $details['databaseerror-error'] = array( 'samp', $attribs, $errorMessage ); + } + + return $details; } /** - * @return String + * @param string $key Message key + * @return string: English message text */ - function getPageTitle() { - return $this->msg( 'databaseerror', 'Database error' ); + private function getFallbackMessage( $key ) { + $messages = array( + 'databaseerror-text' => 'A database query error has occurred. +This may indicate a bug in the software.', + 'databaseerror-textcl' => 'A database query error has occurred.', + 'databaseerror-query' => 'Query: $1', + 'databaseerror-function' => 'Function: $1', + 'databaseerror-error' => 'Error: $1', + ); + return $messages[$key]; } + } /** diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php deleted file mode 100644 index f1f6dfca..00000000 --- a/includes/db/DatabaseIbm_db2.php +++ /dev/null @@ -1,1721 +0,0 @@ -<?php -/** - * This is the IBM DB2 database abstraction layer. - * See maintenance/ibm_db2/README for development notes - * and other specific information. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup Database - * @author leo.petr+mediawiki@gmail.com - */ - -/** - * This represents a column in a DB2 database - * @ingroup Database - */ -class IBM_DB2Field implements Field { - private $name = ''; - private $tablename = ''; - private $type = ''; - private $nullable = false; - private $max_length = 0; - - /** - * Builder method for the class - * @param $db DatabaseIbm_db2: Database interface - * @param $table String: table name - * @param $field String: column name - * @return IBM_DB2Field - */ - static function fromText( $db, $table, $field ) { - global $wgDBmwschema; - - $q = <<<SQL -SELECT -lcase( coltype ) AS typname, -nulls AS attnotnull, length AS attlen -FROM sysibm.syscolumns -WHERE tbcreator=%s AND tbname=%s AND name=%s; -SQL; - $res = $db->query( - sprintf( $q, - $db->addQuotes( $wgDBmwschema ), - $db->addQuotes( $table ), - $db->addQuotes( $field ) - ) - ); - $row = $db->fetchObject( $res ); - if ( !$row ) { - return null; - } - $n = new IBM_DB2Field; - $n->type = $row->typname; - $n->nullable = ( $row->attnotnull == 'N' ); - $n->name = $field; - $n->tablename = $table; - $n->max_length = $row->attlen; - return $n; - } - /** - * Get column name - * @return string column name - */ - function name() { return $this->name; } - /** - * Get table name - * @return string table name - */ - function tableName() { return $this->tablename; } - /** - * Get column type - * @return string column type - */ - function type() { return $this->type; } - /** - * Can column be null? - * @return bool true or false - */ - function isNullable() { return $this->nullable; } - /** - * How much can you fit in the column per row? - * @return int length - */ - function maxLength() { return $this->max_length; } -} - -/** - * Wrapper around binary large objects - * @ingroup Database - */ -class IBM_DB2Blob { - private $mData; - - public function __construct( $data ) { - $this->mData = $data; - } - - public function getData() { - return $this->mData; - } - - public function __toString() { - return $this->mData; - } -} - -/** - * Wrapper to address lack of certain operations in the DB2 driver - * ( seek, num_rows ) - * @ingroup Database - * @since 1.19 - */ -class IBM_DB2Result{ - private $db; - private $result; - private $num_rows; - private $current_pos; - private $columns = array(); - private $sql; - - private $resultSet = array(); - private $loadedLines = 0; - - /** - * Construct and initialize a wrapper for DB2 query results - * @param $db DatabaseBase - * @param $result Object - * @param $num_rows Integer - * @param $sql String - * @param $columns Array - */ - public function __construct( $db, $result, $num_rows, $sql, $columns ){ - $this->db = $db; - - if( $result instanceof ResultWrapper ){ - $this->result = $result->result; - } - else{ - $this->result = $result; - } - - $this->num_rows = $num_rows; - $this->current_pos = 0; - if ( $this->num_rows > 0 ) { - // Make a lower-case list of the column names - // By default, DB2 column names are capitalized - // while MySQL column names are lowercase - - // Is there a reasonable maximum value for $i? - // Setting to 2048 to prevent an infinite loop - for( $i = 0; $i < 2048; $i++ ) { - $name = db2_field_name( $this->result, $i ); - if ( $name != false ) { - continue; - } - else { - return false; - } - - $this->columns[$i] = strtolower( $name ); - } - } - - $this->sql = $sql; - } - - /** - * Unwrap the DB2 query results - * @return mixed Object on success, false on failure - */ - public function getResult() { - if ( $this->result ) { - return $this->result; - } - else return false; - } - - /** - * Get the number of rows in the result set - * @return integer - */ - public function getNum_rows() { - return $this->num_rows; - } - - /** - * Return a row from the result set in object format - * @return mixed Object on success, false on failure. - */ - public function fetchObject() { - if ( $this->result - && $this->num_rows > 0 - && $this->current_pos >= 0 - && $this->current_pos < $this->num_rows ) - { - $row = $this->fetchRow(); - $ret = new stdClass(); - - foreach ( $row as $k => $v ) { - $lc = $this->columns[$k]; - $ret->$lc = $v; - } - return $ret; - } - return false; - } - - /** - * Return a row form the result set in array format - * @return mixed Array on success, false on failure - * @throws DBUnexpectedError - */ - public function fetchRow(){ - if ( $this->result - && $this->num_rows > 0 - && $this->current_pos >= 0 - && $this->current_pos < $this->num_rows ) - { - if ( $this->loadedLines <= $this->current_pos ) { - $row = db2_fetch_array( $this->result ); - $this->resultSet[$this->loadedLines++] = $row; - if ( $this->db->lastErrno() ) { - throw new DBUnexpectedError( $this->db, 'Error in fetchRow(): ' - . htmlspecialchars( $this->db->lastError() ) ); - } - } - - if ( $this->loadedLines > $this->current_pos ){ - return $this->resultSet[$this->current_pos++]; - } - - } - return false; - } - - /** - * Free a DB2 result object - * @throws DBUnexpectedError - */ - public function freeResult(){ - unset( $this->resultSet ); - if ( !@db2_free_result( $this->result ) ) { - throw new DBUnexpectedError( $this, "Unable to free DB2 result\n" ); - } - } -} - -/** - * Primary database interface - * @ingroup Database - */ -class DatabaseIbm_db2 extends DatabaseBase { - /* - * Inherited members - protected $mLastQuery = ''; - protected $mPHPError = false; - - protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname; - protected $mOpened = false; - - protected $mTablePrefix; - protected $mFlags; - protected $mTrxLevel = 0; - protected $mErrorCount = 0; - protected $mLBInfo = array(); - protected $mFakeSlaveLag = null, $mFakeMaster = false; - * - */ - - /** Database server port */ - protected $mPort = null; - /** Schema for tables, stored procedures, triggers */ - protected $mSchema = null; - /** Whether the schema has been applied in this session */ - protected $mSchemaSet = false; - /** Result of last query */ - protected $mLastResult = null; - /** Number of rows affected by last INSERT/UPDATE/DELETE */ - protected $mAffectedRows = null; - /** Number of rows returned by last SELECT */ - protected $mNumRows = null; - /** Current row number on the cursor of the last SELECT */ - protected $currentRow = 0; - - /** Connection config options - see constructor */ - public $mConnOptions = array(); - /** Statement config options -- see constructor */ - public $mStmtOptions = array(); - - /** Default schema */ - const USE_GLOBAL = 'get from global'; - - /** Option that applies to nothing */ - const NONE_OPTION = 0x00; - /** Option that applies to connection objects */ - const CONN_OPTION = 0x01; - /** Option that applies to statement objects */ - const STMT_OPTION = 0x02; - - /** Regular operation mode -- minimal debug messages */ - const REGULAR_MODE = 'regular'; - /** Installation mode -- lots of debug messages */ - const INSTALL_MODE = 'install'; - - /** Controls the level of debug message output */ - protected $mMode = self::REGULAR_MODE; - - /** Last sequence value used for a primary key */ - protected $mInsertId = null; - - ###################################### - # Getters and Setters - ###################################### - - /** - * Returns true if this database supports (and uses) cascading deletes - * @return bool - */ - function cascadingDeletes() { - return true; - } - - /** - * Returns true if this database supports (and uses) triggers (e.g. on the - * page table) - * @return bool - */ - function cleanupTriggers() { - return true; - } - - /** - * Returns true if this database is strict about what can be put into an - * IP field. - * Specifically, it uses a NULL value instead of an empty string. - * @return bool - */ - function strictIPs() { - return true; - } - - /** - * Returns true if this database uses timestamps rather than integers - * @return bool - */ - function realTimestamps() { - return true; - } - - /** - * Returns true if this database does an implicit sort when doing GROUP BY - * @return bool - */ - function implicitGroupby() { - return false; - } - - /** - * Returns true if this database does an implicit order by when the column - * has an index - * For example: SELECT page_title FROM page LIMIT 1 - * @return bool - */ - function implicitOrderby() { - return false; - } - - /** - * Returns true if this database can do a native search on IP columns - * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32'; - * @return bool - */ - function searchableIPs() { - return true; - } - - /** - * Returns true if this database can use functional indexes - * @return bool - */ - function functionalIndexes() { - return true; - } - - /** - * Returns a unique string representing the wiki on the server - * @return string - */ - public function getWikiID() { - if( $this->mSchema ) { - return "{$this->mDBname}-{$this->mSchema}"; - } else { - return $this->mDBname; - } - } - - /** - * Returns the database software identifieir - * @return string - */ - public function getType() { - return 'ibm_db2'; - } - - /** - * Returns the database connection object - * @return Object - */ - public function getDb(){ - return $this->mConn; - } - - /** - * - * @param $server String: hostname of database server - * @param $user String: username - * @param $password String: password - * @param $dbName String: database name on the server - * @param $flags Integer: database behaviour flags (optional, unused) - * @param $schema String - */ - public function __construct( $server = false, $user = false, - $password = false, - $dbName = false, $flags = 0, - $schema = self::USE_GLOBAL ) - { - global $wgDBmwschema; - - if ( $schema == self::USE_GLOBAL ) { - $this->mSchema = $wgDBmwschema; - } else { - $this->mSchema = $schema; - } - - // configure the connection and statement objects - $this->setDB2Option( 'db2_attr_case', 'DB2_CASE_LOWER', - self::CONN_OPTION | self::STMT_OPTION ); - $this->setDB2Option( 'deferred_prepare', 'DB2_DEFERRED_PREPARE_ON', - self::STMT_OPTION ); - $this->setDB2Option( 'rowcount', 'DB2_ROWCOUNT_PREFETCH_ON', - self::STMT_OPTION ); - parent::__construct( $server, $user, $password, $dbName, DBO_TRX | $flags ); - } - - /** - * Enables options only if the ibm_db2 extension version supports them - * @param $name String: name of the option in the options array - * @param $const String: name of the constant holding the right option value - * @param $type Integer: whether this is a Connection or Statement otion - */ - private function setDB2Option( $name, $const, $type ) { - if ( defined( $const ) ) { - if ( $type & self::CONN_OPTION ) { - $this->mConnOptions[$name] = constant( $const ); - } - if ( $type & self::STMT_OPTION ) { - $this->mStmtOptions[$name] = constant( $const ); - } - } else { - $this->installPrint( - "$const is not defined. ibm_db2 version is likely too low." ); - } - } - - /** - * Outputs debug information in the appropriate place - * @param $string String: the relevant debug message - */ - private function installPrint( $string ) { - wfDebug( "$string\n" ); - if ( $this->mMode == self::INSTALL_MODE ) { - print "<li><pre>$string</pre></li>"; - flush(); - } - } - - /** - * Opens a database connection and returns it - * Closes any existing connection - * - * @param $server String: hostname - * @param $user String - * @param $password String - * @param $dbName String: database name - * @return DatabaseBase a fresh connection - */ - public function open( $server, $user, $password, $dbName ) { - wfProfileIn( __METHOD__ ); - - # Load IBM DB2 driver if missing - wfDl( 'ibm_db2' ); - - # Test for IBM DB2 support, to avoid suppressed fatal error - if ( !function_exists( 'db2_connect' ) ) { - throw new DBConnectionError( $this, "DB2 functions missing, have you enabled the ibm_db2 extension for PHP?" ); - } - - global $wgDBport; - - // Close existing connection - $this->close(); - // Cache conn info - $this->mServer = $server; - $this->mPort = $port = $wgDBport; - $this->mUser = $user; - $this->mPassword = $password; - $this->mDBname = $dbName; - - $this->openUncataloged( $dbName, $user, $password, $server, $port ); - - if ( !$this->mConn ) { - $this->installPrint( "DB connection error\n" ); - $this->installPrint( - "Server: $server, Database: $dbName, User: $user, Password: " - . substr( $password, 0, 3 ) . "...\n" ); - $this->installPrint( $this->lastError() . "\n" ); - wfProfileOut( __METHOD__ ); - wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); - wfDebug( $this->lastError() . "\n" ); - throw new DBConnectionError( $this, $this->lastError() ); - } - - // Some MediaWiki code is still transaction-less (?). - // The strategy is to keep AutoCommit on for that code - // but switch it off whenever a transaction is begun. - db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON ); - - $this->mOpened = true; - $this->applySchema(); - - wfProfileOut( __METHOD__ ); - return $this->mConn; - } - - /** - * Opens a cataloged database connection, sets mConn - */ - protected function openCataloged( $dbName, $user, $password ) { - wfSuppressWarnings(); - $this->mConn = db2_pconnect( $dbName, $user, $password ); - wfRestoreWarnings(); - } - - /** - * Opens an uncataloged database connection, sets mConn - */ - protected function openUncataloged( $dbName, $user, $password, $server, $port ) - { - $dsn = "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=$dbName;CHARSET=UTF-8;HOSTNAME=$server;PORT=$port;PROTOCOL=TCPIP;UID=$user;PWD=$password;"; - wfSuppressWarnings(); - $this->mConn = db2_pconnect( $dsn, "", "", array() ); - wfRestoreWarnings(); - } - - /** - * Closes a database connection, if it is open - * Returns success, true if already closed - * @return bool - */ - protected function closeConnection() { - return db2_close( $this->mConn ); - } - - /** - * Retrieves the most current database error - * Forces a database rollback - * @return bool|string - */ - public function lastError() { - $connerr = db2_conn_errormsg(); - if ( $connerr ) { - //$this->rollback( __METHOD__ ); - return $connerr; - } - $stmterr = db2_stmt_errormsg(); - if ( $stmterr ) { - //$this->rollback( __METHOD__ ); - return $stmterr; - } - - return false; - } - - /** - * Get the last error number - * Return 0 if no error - * @return integer - */ - public function lastErrno() { - $connerr = db2_conn_error(); - if ( $connerr ) { - return $connerr; - } - $stmterr = db2_stmt_error(); - if ( $stmterr ) { - return $stmterr; - } - return 0; - } - - /** - * Is a database connection open? - * @return - */ - public function isOpen() { return $this->mOpened; } - - /** - * The DBMS-dependent part of query() - * @param $sql String: SQL query. - * @return object Result object for fetch functions or false on failure - */ - protected function doQuery( $sql ) { - $this->applySchema(); - - // Needed to handle any UTF-8 encoding issues in the raw sql - // Note that we fully support prepared statements for DB2 - // prepare() and execute() should be used instead of doQuery() whenever possible - $sql = utf8_decode( $sql ); - - $ret = db2_exec( $this->mConn, $sql, $this->mStmtOptions ); - if( $ret == false ) { - $error = db2_stmt_errormsg(); - - $this->installPrint( "<pre>$sql</pre>" ); - $this->installPrint( $error ); - throw new DBUnexpectedError( $this, 'SQL error: ' - . htmlspecialchars( $error ) ); - } - $this->mLastResult = $ret; - $this->mAffectedRows = null; // Not calculated until asked for - return $ret; - } - - /** - * @return string Version information from the database - */ - public function getServerVersion() { - $info = db2_server_info( $this->mConn ); - return $info->DBMS_VER; - } - - /** - * Queries whether a given table exists - * @return boolean - */ - public function tableExists( $table, $fname = __METHOD__ ) { - $schema = $this->mSchema; - - $sql = "SELECT COUNT( * ) FROM SYSIBM.SYSTABLES ST WHERE ST.NAME = '" . - strtoupper( $table ) . - "' AND ST.CREATOR = '" . - strtoupper( $schema ) . "'"; - $res = $this->query( $sql ); - if ( !$res ) { - return false; - } - - // If the table exists, there should be one of it - $row = $this->fetchRow( $res ); - $count = $row[0]; - if ( $count == '1' || $count == 1 ) { - return true; - } - - return false; - } - - /** - * 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. - * - * @param $res array|ResultWrapper SQL result object as returned from Database::query(), etc. - * @return DB2 row object - * @throws DBUnexpectedError Thrown if the database returns an error - */ - public function fetchObject( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - wfSuppressWarnings(); - $row = db2_fetch_object( $res ); - wfRestoreWarnings(); - if( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' - . htmlspecialchars( $this->lastError() ) ); - } - return $row; - } - - /** - * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. - * - * @param $res array|ResultWrapper SQL result object as returned from Database::query(), etc. - * @return ResultWrapper row object - * @throws DBUnexpectedError Thrown if the database returns an error - */ - public function fetchRow( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - if ( db2_num_rows( $res ) > 0) { - wfSuppressWarnings(); - $row = db2_fetch_array( $res ); - wfRestoreWarnings(); - if ( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' - . htmlspecialchars( $this->lastError() ) ); - } - return $row; - } - return false; - } - - /** - * Escapes strings - * Doesn't escape numbers - * - * @param $s String: string to escape - * @return string escaped string - */ - public function addQuotes( $s ) { - //$this->installPrint( "DB2::addQuotes( $s )\n" ); - if ( is_null( $s ) ) { - return 'NULL'; - } elseif ( $s instanceof Blob ) { - return "'" . $s->fetch( $s ) . "'"; - } elseif ( $s instanceof IBM_DB2Blob ) { - return "'" . $this->decodeBlob( $s ) . "'"; - } - $s = $this->strencode( $s ); - if ( is_numeric( $s ) ) { - return $s; - } else { - return "'$s'"; - } - } - - /** - * Verifies that a DB2 column/field type is numeric - * - * @param $type String: DB2 column type - * @return Boolean: true if numeric - */ - public function is_numeric_type( $type ) { - switch ( strtoupper( $type ) ) { - case 'SMALLINT': - case 'INTEGER': - case 'INT': - case 'BIGINT': - case 'DECIMAL': - case 'REAL': - case 'DOUBLE': - case 'DECFLOAT': - return true; - } - return false; - } - - /** - * Alias for addQuotes() - * @param $s String: string to escape - * @return string escaped string - */ - public function strencode( $s ) { - // Bloody useless function - // Prepends backslashes to \x00, \n, \r, \, ', " and \x1a. - // But also necessary - $s = db2_escape_string( $s ); - // Wide characters are evil -- some of them look like ' - $s = utf8_encode( $s ); - // Fix its stupidity - $from = array( "\\\\", "\\'", '\\n', '\\t', '\\"', '\\r' ); - $to = array( "\\", "''", "\n", "\t", '"', "\r" ); - $s = str_replace( $from, $to, $s ); // DB2 expects '', not \' escaping - return $s; - } - - /** - * Switch into the database schema - */ - protected function applySchema() { - if ( !( $this->mSchemaSet ) ) { - $this->mSchemaSet = true; - $this->begin( __METHOD__ ); - $this->doQuery( "SET SCHEMA = $this->mSchema" ); - $this->commit( __METHOD__ ); - } - } - - /** - * Start a transaction (mandatory) - */ - protected function doBegin( $fname = 'DatabaseIbm_db2::begin' ) { - // BEGIN is implicit for DB2 - // However, it requires that AutoCommit be off. - - // Some MediaWiki code is still transaction-less (?). - // The strategy is to keep AutoCommit on for that code - // but switch it off whenever a transaction is begun. - db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_OFF ); - - $this->mTrxLevel = 1; - } - - /** - * End a transaction - * Must have a preceding begin() - */ - protected function doCommit( $fname = 'DatabaseIbm_db2::commit' ) { - db2_commit( $this->mConn ); - - // Some MediaWiki code is still transaction-less (?). - // The strategy is to keep AutoCommit on for that code - // but switch it off whenever a transaction is begun. - db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON ); - - $this->mTrxLevel = 0; - } - - /** - * Cancel a transaction - */ - protected function doRollback( $fname = 'DatabaseIbm_db2::rollback' ) { - db2_rollback( $this->mConn ); - // turn auto-commit back on - // not sure if this is appropriate - db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON ); - $this->mTrxLevel = 0; - } - - /** - * Makes an encoded list of strings from an array - * $mode: - * LIST_COMMA - comma separated, no field names - * LIST_AND - ANDed WHERE clause (without the WHERE) - * LIST_OR - ORed WHERE clause (without the WHERE) - * LIST_SET - comma separated with field names, like a SET clause - * LIST_NAMES - comma separated field names - * LIST_SET_PREPARED - like LIST_SET, except with ? tokens as values - * @return string - */ - function makeList( $a, $mode = LIST_COMMA ) { - if ( !is_array( $a ) ) { - throw new DBUnexpectedError( $this, - 'DatabaseIbm_db2::makeList called with incorrect parameters' ); - } - - // if this is for a prepared UPDATE statement - // (this should be promoted to the parent class - // once other databases use prepared statements) - if ( $mode == LIST_SET_PREPARED ) { - $first = true; - $list = ''; - foreach ( $a as $field => $value ) { - if ( !$first ) { - $list .= ", $field = ?"; - } else { - $list .= "$field = ?"; - $first = false; - } - } - $list .= ''; - - return $list; - } - - // otherwise, call the usual function - return parent::makeList( $a, $mode ); - } - - /** - * Construct a LIMIT query with optional offset - * This is used for query pages - * - * @param $sql string SQL query we will append the limit too - * @param $limit integer the SQL limit - * @param $offset integer the SQL offset (default false) - * @return string - */ - public function limitResult( $sql, $limit, $offset=false ) { - if( !is_numeric( $limit ) ) { - throw new DBUnexpectedError( $this, - "Invalid non-numeric limit passed to limitResult()\n" ); - } - if( $offset ) { - if ( stripos( $sql, 'where' ) === false ) { - return "$sql AND ( ROWNUM BETWEEN $offset AND $offset+$limit )"; - } else { - return "$sql WHERE ( ROWNUM BETWEEN $offset AND $offset+$limit )"; - } - } - return "$sql FETCH FIRST $limit ROWS ONLY "; - } - - /** - * Handle reserved keyword replacement in table names - * - * @param $name Object - * @param $format String Ignored parameter Default 'quoted'Boolean - * @return String - */ - public function tableName( $name, $format = 'quoted' ) { - // we want maximum compatibility with MySQL schema - return $name; - } - - /** - * Generates a timestamp in an insertable format - * - * @param $ts string timestamp - * @return String: timestamp value - */ - public function timestamp( $ts = 0 ) { - // TS_MW cannot be easily distinguished from an integer - return wfTimestamp( TS_DB2, $ts ); - } - - /** - * Return the next in a sequence, save the value for retrieval via insertId() - * @param $seqName String: name of a defined sequence in the database - * @return int next value in that sequence - */ - public function nextSequenceValue( $seqName ) { - // Not using sequences in the primary schema to allow for easier migration - // from MySQL - // Emulating MySQL behaviour of using NULL to signal that sequences - // aren't used - /* - $safeseq = preg_replace( "/'/", "''", $seqName ); - $res = $this->query( "VALUES NEXTVAL FOR $safeseq" ); - $row = $this->fetchRow( $res ); - $this->mInsertId = $row[0]; - return $this->mInsertId; - */ - return null; - } - - /** - * This must be called after nextSequenceVal - * @return int Last sequence value used as a primary key - */ - public function insertId() { - return $this->mInsertId; - } - - /** - * Updates the mInsertId property with the value of the last insert - * into a generated column - * - * @param $table String: sanitized table name - * @param $primaryKey Mixed: string name of the primary key - * @param $stmt Resource: prepared statement resource - * of the SELECT primary_key FROM FINAL TABLE ( INSERT ... ) form - */ - private function calcInsertId( $table, $primaryKey, $stmt ) { - if ( $primaryKey ) { - $this->mInsertId = db2_last_insert_id( $this->mConn ); - } - } - - /** - * INSERT wrapper, inserts an array into a table - * - * $args may be a single associative array, or an array of arrays - * with numeric keys, for multi-row insert - * - * @param $table String: Name of the table to insert to. - * @param $args Array: Items to insert into the table. - * @param $fname String: Name of the function, for profiling - * @param $options String or Array. Valid options: IGNORE - * - * @return bool Success of insert operation. IGNORE always returns true. - */ - public function insert( $table, $args, $fname = 'DatabaseIbm_db2::insert', - $options = array() ) - { - if ( !count( $args ) ) { - return true; - } - // get database-specific table name (not used) - $table = $this->tableName( $table ); - // format options as an array - $options = IBM_DB2Helper::makeArray( $options ); - // format args as an array of arrays - if ( !( isset( $args[0] ) && is_array( $args[0] ) ) ) { - $args = array( $args ); - } - - // prevent insertion of NULL into primary key columns - list( $args, $primaryKeys ) = $this->removeNullPrimaryKeys( $table, $args ); - // if there's only one primary key - // we'll be able to read its value after insertion - $primaryKey = false; - if ( count( $primaryKeys ) == 1 ) { - $primaryKey = $primaryKeys[0]; - } - - // get column names - $keys = array_keys( $args[0] ); - $key_count = count( $keys ); - - // If IGNORE is set, we use savepoints to emulate mysql's behavior - $ignore = in_array( 'IGNORE', $options ) ? 'mw' : ''; - - // assume success - $res = true; - // If we are not in a transaction, we need to be for savepoint trickery - if ( !$this->mTrxLevel ) { - $this->begin( __METHOD__ ); - } - - $sql = "INSERT INTO $table ( " . implode( ',', $keys ) . ' ) VALUES '; - if ( $key_count == 1 ) { - $sql .= '( ? )'; - } else { - $sql .= '( ?' . str_repeat( ',?', $key_count-1 ) . ' )'; - } - $this->installPrint( "Preparing the following SQL:" ); - $this->installPrint( "$sql" ); - $this->installPrint( print_r( $args, true )); - $stmt = $this->prepare( $sql ); - - // start a transaction/enter transaction mode - $this->begin( __METHOD__ ); - - if ( !$ignore ) { - //$first = true; - foreach ( $args as $row ) { - //$this->installPrint( "Inserting " . print_r( $row, true )); - // insert each row into the database - $res = $res & $this->execute( $stmt, $row ); - if ( !$res ) { - $this->installPrint( 'Last error:' ); - $this->installPrint( $this->lastError() ); - } - // get the last inserted value into a generated column - $this->calcInsertId( $table, $primaryKey, $stmt ); - } - } else { - $olde = error_reporting( 0 ); - // For future use, we may want to track the number of actual inserts - // Right now, insert (all writes) simply return true/false - $numrowsinserted = 0; - - // always return true - $res = true; - - foreach ( $args as $row ) { - $overhead = "SAVEPOINT $ignore ON ROLLBACK RETAIN CURSORS"; - db2_exec( $this->mConn, $overhead, $this->mStmtOptions ); - - $res2 = $this->execute( $stmt, $row ); - - if ( !$res2 ) { - $this->installPrint( 'Last error:' ); - $this->installPrint( $this->lastError() ); - } - // get the last inserted value into a generated column - $this->calcInsertId( $table, $primaryKey, $stmt ); - - $errNum = $this->lastErrno(); - if ( $errNum ) { - db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore", - $this->mStmtOptions ); - } else { - db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore", - $this->mStmtOptions ); - $numrowsinserted++; - } - } - - $olde = error_reporting( $olde ); - // Set the affected row count for the whole operation - $this->mAffectedRows = $numrowsinserted; - } - // commit either way - $this->commit( __METHOD__ ); - $this->freePrepared( $stmt ); - - return $res; - } - - /** - * Given a table name and a hash of columns with values - * Removes primary key columns from the hash where the value is NULL - * - * @param $table String: name of the table - * @param $args Array of hashes of column names with values - * @return Array: tuple( filtered array of columns, array of primary keys ) - */ - private function removeNullPrimaryKeys( $table, $args ) { - $schema = $this->mSchema; - - // find out the primary keys - $keyres = $this->doQuery( "SELECT NAME FROM SYSIBM.SYSCOLUMNS WHERE TBNAME = '" - . strtoupper( $table ) - . "' AND TBCREATOR = '" - . strtoupper( $schema ) - . "' AND KEYSEQ > 0" ); - - $keys = array(); - for ( - $row = $this->fetchRow( $keyres ); - $row != null; - $row = $this->fetchRow( $keyres ) - ) - { - $keys[] = strtolower( $row[0] ); - } - // remove primary keys - foreach ( $args as $ai => $row ) { - foreach ( $keys as $key ) { - if ( $row[$key] == null ) { - unset( $row[$key] ); - } - } - $args[$ai] = $row; - } - // return modified hash - return array( $args, $keys ); - } - - /** - * UPDATE wrapper, takes a condition array and a SET array - * - * @param $table String: The table to UPDATE - * @param $values array An array of values to SET - * @param $conds array An array of conditions ( WHERE ). Use '*' to update all rows. - * @param $fname String: The Class::Function calling this function - * ( for the log ) - * @param $options array An array of UPDATE options, can be one or - * more of IGNORE, LOW_PRIORITY - * @return Boolean - */ - public function update( $table, $values, $conds, $fname = 'DatabaseIbm_db2::update', - $options = array() ) - { - $table = $this->tableName( $table ); - $opts = $this->makeUpdateOptions( $options ); - $sql = "UPDATE $opts $table SET " - . $this->makeList( $values, LIST_SET_PREPARED ); - if ( $conds != '*' ) { - $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); - } - $stmt = $this->prepare( $sql ); - $this->installPrint( 'UPDATE: ' . print_r( $values, true ) ); - // assuming for now that an array with string keys will work - // if not, convert to simple array first - $result = $this->execute( $stmt, $values ); - $this->freePrepared( $stmt ); - - return $result; - } - - /** - * DELETE query wrapper - * - * Use $conds == "*" to delete all rows - * @return bool|\ResultWrapper - */ - public function delete( $table, $conds, $fname = 'DatabaseIbm_db2::delete' ) { - if ( !$conds ) { - throw new DBUnexpectedError( $this, - 'DatabaseIbm_db2::delete() called with no conditions' ); - } - $table = $this->tableName( $table ); - $sql = "DELETE FROM $table"; - if ( $conds != '*' ) { - $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); - } - $result = $this->query( $sql, $fname ); - - return $result; - } - - /** - * Returns the number of rows affected by the last query or 0 - * @return Integer: the number of rows affected by the last query - */ - public function affectedRows() { - if ( !is_null( $this->mAffectedRows ) ) { - // Forced result for simulated queries - return $this->mAffectedRows; - } - if( empty( $this->mLastResult ) ) { - return 0; - } - return db2_num_rows( $this->mLastResult ); - } - - /** - * Returns the number of rows in the result set - * Has to be called right after the corresponding select query - * @param $res Object result set - * @return Integer: number of rows - */ - public function numRows( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - - if ( $this->mNumRows ) { - return $this->mNumRows; - } else { - return 0; - } - } - - /** - * Moves the row pointer of the result set - * @param $res Object: result set - * @param $row Integer: row number - * @return bool success or failure - */ - public function dataSeek( $res, $row ) { - if ( $res instanceof ResultWrapper ) { - return $res = $res->result; - } - if ( $res instanceof IBM_DB2Result ) { - return $res->dataSeek( $row ); - } - wfDebug( "dataSeek operation in DB2 database\n" ); - return false; - } - - ### - # Fix notices in Block.php - ### - - /** - * Frees memory associated with a statement resource - * @param $res Object: statement resource to free - * @return Boolean success or failure - */ - public function freeResult( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - wfSuppressWarnings(); - $ok = db2_free_result( $res ); - wfRestoreWarnings(); - if ( !$ok ) { - throw new DBUnexpectedError( $this, "Unable to free DB2 result\n" ); - } - } - - /** - * Returns the number of columns in a resource - * @param $res Object: statement resource - * @return Number of fields/columns in resource - */ - public function numFields( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - if ( $res instanceof IBM_DB2Result ) { - $res = $res->getResult(); - } - return db2_num_fields( $res ); - } - - /** - * Returns the nth column name - * @param $res Object: statement resource - * @param $n Integer: Index of field or column - * @return String name of nth column - */ - public function fieldName( $res, $n ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - if ( $res instanceof IBM_DB2Result ) { - $res = $res->getResult(); - } - return db2_field_name( $res, $n ); - } - - /** - * SELECT wrapper - * - * @param $table Array or string, table name(s) (prefix auto-added) - * @param $vars Array or string, field name(s) to be retrieved - * @param $conds Array or string, condition(s) for WHERE - * @param $fname String: calling function name (use __METHOD__) - * for logs/profiling - * @param $options array Associative array of options - * (e.g. array( 'GROUP BY' => 'page_title' )), - * see Database::makeSelectOptions code for list of - * supported stuff - * @param $join_conds array Associative array of table join conditions (optional) - * (e.g. array( 'page' => array('LEFT JOIN', - * 'page_latest=rev_id') ) - * @return Mixed: database result resource for fetch functions or false - * on failure - */ - public function select( $table, $vars, $conds = '', $fname = 'DatabaseIbm_db2::select', $options = array(), $join_conds = array() ) - { - $res = parent::select( $table, $vars, $conds, $fname, $options, - $join_conds ); - $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); - - // We must adjust for offset - if ( isset( $options['LIMIT'] ) && isset ( $options['OFFSET'] ) ) { - $limit = $options['LIMIT']; - $offset = $options['OFFSET']; - } - - // DB2 does not have a proper num_rows() function yet, so we must emulate - // DB2 9.5.4 and the corresponding ibm_db2 driver will introduce - // a working one - // TODO: Yay! - - // we want the count - $vars2 = array( 'count( * ) as num_rows' ); - // respecting just the limit option - $options2 = array(); - if ( isset( $options['LIMIT'] ) ) { - $options2['LIMIT'] = $options['LIMIT']; - } - // but don't try to emulate for GROUP BY - if ( isset( $options['GROUP BY'] ) ) { - return $res; - } - - $res2 = parent::select( $table, $vars2, $conds, $fname, $options2, - $join_conds ); - - $obj = $this->fetchObject( $res2 ); - $this->mNumRows = $obj->num_rows; - - return new ResultWrapper( $this, new IBM_DB2Result( $this, $res, $obj->num_rows, $vars, $sql ) ); - } - - /** - * Handles ordering, grouping, and having options ('GROUP BY' => colname) - * Has limited support for per-column options (colnum => 'DISTINCT') - * - * @private - * - * @param $options array Associative array of options to be turned into - * an SQL query, valid keys are listed in the function. - * @return Array - */ - function makeSelectOptions( $options ) { - $preLimitTail = $postLimitTail = ''; - $startOpts = ''; - - $noKeyOptions = array(); - foreach ( $options as $key => $option ) { - if ( is_numeric( $key ) ) { - $noKeyOptions[$option] = true; - } - } - - if ( isset( $options['GROUP BY'] ) ) { - $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; - } - if ( isset( $options['HAVING'] ) ) { - $preLimitTail .= " HAVING {$options['HAVING']}"; - } - if ( isset( $options['ORDER BY'] ) ) { - $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; - } - - if ( isset( $noKeyOptions['DISTINCT'] ) - || isset( $noKeyOptions['DISTINCTROW'] ) ) - { - $startOpts .= 'DISTINCT'; - } - - return array( $startOpts, '', $preLimitTail, $postLimitTail ); - } - - /** - * Returns link to IBM DB2 free download - * @return String: wikitext of a link to the server software's web site - */ - public static function getSoftwareLink() { - return '[http://www.ibm.com/db2/express/ IBM DB2]'; - } - - /** - * Get search engine class. All subclasses of this - * need to implement this if they wish to use searching. - * - * @return String - */ - public function getSearchEngine() { - return 'SearchIBM_DB2'; - } - - /** - * Did the last database access fail because of deadlock? - * @return Boolean - */ - public function wasDeadlock() { - // get SQLSTATE - $err = $this->lastErrno(); - switch( $err ) { - // This is literal port of the MySQL logic and may be wrong for DB2 - case '40001': // sql0911n, Deadlock or timeout, rollback - case '57011': // sql0904n, Resource unavailable, no rollback - case '57033': // sql0913n, Deadlock or timeout, no rollback - $this->installPrint( "In a deadlock because of SQLSTATE $err" ); - return true; - } - return false; - } - - /** - * Ping the server and try to reconnect if it there is no connection - * The connection may be closed and reopened while this happens - * @return Boolean: whether the connection exists - */ - public function ping() { - // db2_ping() doesn't exist - // Emulate - $this->close(); - $this->openUncataloged( $this->mDBName, $this->mUser, - $this->mPassword, $this->mServer, $this->mPort ); - - return false; - } - ###################################### - # Unimplemented and not applicable - ###################################### - - /** - * Only useful with fake prepare like in base Database class - * @return string - */ - public function fillPreparedArg( $matches ) { - $this->installPrint( 'Not useful for DB2: fillPreparedArg()' ); - return ''; - } - - ###################################### - # Reflection - ###################################### - - /** - * Returns information about an index - * If errors are explicitly ignored, returns NULL on failure - * @param $table String: table name - * @param $index String: index name - * @param $fname String: function name for logging and profiling - * @return Object query row in object form - */ - public function indexInfo( $table, $index, - $fname = 'DatabaseIbm_db2::indexExists' ) - { - $table = $this->tableName( $table ); - $sql = <<<SQL -SELECT name as indexname -FROM sysibm.sysindexes si -WHERE si.name='$index' AND si.tbname='$table' -AND sc.tbcreator='$this->mSchema' -SQL; - $res = $this->query( $sql, $fname ); - if ( !$res ) { - return null; - } - $row = $this->fetchObject( $res ); - if ( $row != null ) { - return $row; - } else { - return false; - } - } - - /** - * Returns an information object on a table column - * @param $table String: table name - * @param $field String: column name - * @return IBM_DB2Field - */ - public function fieldInfo( $table, $field ) { - return IBM_DB2Field::fromText( $this, $table, $field ); - } - - /** - * db2_field_type() wrapper - * @param $res Object: result of executed statement - * @param $index Mixed: number or name of the column - * @return String column type - */ - public function fieldType( $res, $index ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - if ( $res instanceof IBM_DB2Result ) { - $res = $res->getResult(); - } - return db2_field_type( $res, $index ); - } - - /** - * Verifies that an index was created as unique - * @param $table String: table name - * @param $index String: index name - * @param $fname string function name for profiling - * @return Bool - */ - public function indexUnique ( $table, $index, - $fname = 'DatabaseIbm_db2::indexUnique' ) - { - $table = $this->tableName( $table ); - $sql = <<<SQL -SELECT si.name as indexname -FROM sysibm.sysindexes si -WHERE si.name='$index' AND si.tbname='$table' -AND sc.tbcreator='$this->mSchema' -AND si.uniquerule IN ( 'U', 'P' ) -SQL; - $res = $this->query( $sql, $fname ); - if ( !$res ) { - return null; - } - if ( $this->fetchObject( $res ) ) { - return true; - } - return false; - - } - - /** - * Returns the size of a text field, or -1 for "unlimited" - * @param $table String: table name - * @param $field String: column name - * @return Integer: length or -1 for unlimited - */ - public function textFieldSize( $table, $field ) { - $table = $this->tableName( $table ); - $sql = <<<SQL -SELECT length as size -FROM sysibm.syscolumns sc -WHERE sc.name='$field' AND sc.tbname='$table' -AND sc.tbcreator='$this->mSchema' -SQL; - $res = $this->query( $sql ); - $row = $this->fetchObject( $res ); - $size = $row->size; - return $size; - } - - /** - * Description is left as an exercise for the reader - * @param $b Mixed: data to be encoded - * @return IBM_DB2Blob - */ - public function encodeBlob( $b ) { - return new IBM_DB2Blob( $b ); - } - - /** - * Description is left as an exercise for the reader - * @param $b IBM_DB2Blob: data to be decoded - * @return mixed - */ - public function decodeBlob( $b ) { - return "$b"; - } - - /** - * Convert into a list of string being concatenated - * @param $stringList Array: strings that need to be joined together - * by the SQL engine - * @return String: joined by the concatenation operator - */ - public function buildConcat( $stringList ) { - // || is equivalent to CONCAT - // Sample query: VALUES 'foo' CONCAT 'bar' CONCAT 'baz' - return implode( ' || ', $stringList ); - } - - /** - * Generates the SQL required to convert a DB2 timestamp into a Unix epoch - * @param $column String: name of timestamp column - * @return String: SQL code - */ - public function extractUnixEpoch( $column ) { - // TODO - // see SpecialAncientpages - } - - ###################################### - # Prepared statements - ###################################### - - /** - * Intended to be compatible with the PEAR::DB wrapper functions. - * http://pear.php.net/manual/en/package.database.db.intro-execute.php - * - * ? = scalar value, quoted as necessary - * ! = raw SQL bit (a function for instance) - * & = filename; reads the file and inserts as a blob - * (we don't use this though...) - * @param $sql String: SQL statement with appropriate markers - * @param $func String: Name of the function, for profiling - * @return resource a prepared DB2 SQL statement - */ - public function prepare( $sql, $func = 'DB2::prepare' ) { - $stmt = db2_prepare( $this->mConn, $sql, $this->mStmtOptions ); - return $stmt; - } - - /** - * Frees resources associated with a prepared statement - * @return Boolean success or failure - */ - public function freePrepared( $prepared ) { - return db2_free_stmt( $prepared ); - } - - /** - * Execute a prepared query with the various arguments - * @param $prepared String: the prepared sql - * @param $args Mixed: either an array here, or put scalars as varargs - * @return Resource: results object - */ - public function execute( $prepared, $args = null ) { - if( !is_array( $args ) ) { - # Pull the var args - $args = func_get_args(); - array_shift( $args ); - } - $res = db2_execute( $prepared, $args ); - if ( !$res ) { - $this->installPrint( db2_stmt_errormsg() ); - } - return $res; - } - - /** - * For faking prepared SQL statements on DBs that don't support - * it directly. - * @param $preparedQuery String: a 'preparable' SQL statement - * @param $args Array of arguments to fill it with - * @return String: executable statement - */ - public function fillPrepared( $preparedQuery, $args ) { - reset( $args ); - $this->preparedArgs =& $args; - - foreach ( $args as $i => $arg ) { - db2_bind_param( $preparedQuery, $i+1, $args[$i] ); - } - - return $preparedQuery; - } - - /** - * Switches module between regular and install modes - * @return string - */ - public function setMode( $mode ) { - $old = $this->mMode; - $this->mMode = $mode; - return $old; - } - - /** - * Bitwise negation of a column or value in SQL - * Same as (~field) in C - * @param $field String - * @return String - */ - function bitNot( $field ) { - // expecting bit-fields smaller than 4bytes - return "BITNOT( $field )"; - } - - /** - * Bitwise AND of two columns or values in SQL - * Same as (fieldLeft & fieldRight) in C - * @param $fieldLeft String - * @param $fieldRight String - * @return String - */ - function bitAnd( $fieldLeft, $fieldRight ) { - return "BITAND( $fieldLeft, $fieldRight )"; - } - - /** - * Bitwise OR of two columns or values in SQL - * Same as (fieldLeft | fieldRight) in C - * @param $fieldLeft String - * @param $fieldRight String - * @return String - */ - function bitOr( $fieldLeft, $fieldRight ) { - return "BITOR( $fieldLeft, $fieldRight )"; - } -} - -class IBM_DB2Helper { - public static function makeArray( $maybeArray ) { - if ( !is_array( $maybeArray ) ) { - return array( $maybeArray ); - } - - return $maybeArray; - } -} diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 914ab408..37f5372e 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -28,39 +28,51 @@ * @ingroup Database */ class DatabaseMssql extends DatabaseBase { - var $mInsertId = NULL; - var $mLastResult = NULL; - var $mAffectedRows = NULL; + var $mInsertId = null; + var $mLastResult = null; + var $mAffectedRows = null; var $mPort; function cascadingDeletes() { return true; } + function cleanupTriggers() { return true; } + function strictIPs() { return true; } + function realTimestamps() { return true; } + function implicitGroupby() { return false; } + function implicitOrderby() { return false; } + function functionalIndexes() { return true; } + function unionSupportsOrderAndLimit() { return false; } /** * Usually aborts on failure + * @param string $server + * @param string $user + * @param string $password + * @param string $dbName + * @throws DBConnectionError * @return bool|DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { @@ -84,7 +96,7 @@ class DatabaseMssql extends DatabaseBase { $connectionInfo = array(); - if( $dbName ) { + if ( $dbName ) { $connectionInfo['Database'] = $dbName; } @@ -97,7 +109,7 @@ class DatabaseMssql extends DatabaseBase { $ntAuthPassTest = strtolower( $password ); // Decide which auth scenerio to use - if( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ){ + if ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) { // Don't add credentials to $connectionInfo } else { $connectionInfo['UID'] = $user; @@ -139,11 +151,11 @@ class DatabaseMssql extends DatabaseBase { // $this->limitResult(); if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) { // massage LIMIT -> TopN - $sql = $this->LimitToTopN( $sql ) ; + $sql = $this->LimitToTopN( $sql ); } // MSSQL doesn't have EXTRACT(epoch FROM XXX) - if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) { + if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) { // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql ); } @@ -151,7 +163,7 @@ class DatabaseMssql extends DatabaseBase { // perform query $stmt = sqlsrv_query( $this->mConn, $sql ); if ( $stmt == false ) { - $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . + $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . "Query: " . htmlentities( $sql ) . "\n" . "Function: " . __METHOD__ . "\n"; // process each error (our driver will give us an array of errors unlike other providers) @@ -197,9 +209,9 @@ class DatabaseMssql extends DatabaseBase { $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL ); if ( $retErrors != null ) { foreach ( $retErrors as $arrError ) { - $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n"; - $strRet .= "Error Code: " . $arrError[ 'code'] . "\n"; - $strRet .= "Message: " . $arrError[ 'message'] . "\n"; + $strRet .= "SQLState: " . $arrError['SQLSTATE'] . "\n"; + $strRet .= "Error Code: " . $arrError['code'] . "\n"; + $strRet .= "Message: " . $arrError['message'] . "\n"; } } else { $strRet = "No errors found"; @@ -279,13 +291,13 @@ class DatabaseMssql extends DatabaseBase { * @param $vars Mixed: array or string, field name(s) to be retrieved * @param $conds Mixed: array or string, condition(s) for WHERE * @param $fname String: calling function name (use __METHOD__) for logs/profiling - * @param $options Array: associative array of options (e.g. array('GROUP BY' => 'page_title')), + * @param array $options associative array of options (e.g. array('GROUP BY' => 'page_title')), * see Database::makeSelectOptions code for list of supported stuff * @param $join_conds Array: Associative array of table join conditions (optional) * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure */ - function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) + function select( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) { $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); if ( isset( $options['EXPLAIN'] ) ) { @@ -304,17 +316,17 @@ class DatabaseMssql extends DatabaseBase { * @param $vars Mixed: Array or string, field name(s) to be retrieved * @param $conds Mixed: Array or string, condition(s) for WHERE * @param $fname String: Calling function name (use __METHOD__) for logs/profiling - * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), * see Database::makeSelectOptions code for list of supported stuff * @param $join_conds Array: Associative array of table join conditions (optional) * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) * @return string, the SQL text */ - function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) { + function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) { if ( isset( $options['EXPLAIN'] ) ) { unset( $options['EXPLAIN'] ); } - return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); + return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); } /** @@ -325,14 +337,16 @@ class DatabaseMssql extends DatabaseBase { * Takes same arguments as Database::select() * @return int */ - function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) { + function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) { $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx $res = $this->select( $table, $vars, $conds, $fname, $options ); $rows = -1; if ( $res ) { $row = $this->fetchRow( $res ); - if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows']; + if ( isset( $row['EstimateRows'] ) ) { + $rows = $row['EstimateRows']; + } } return $rows; } @@ -342,13 +356,13 @@ class DatabaseMssql extends DatabaseBase { * If errors are explicitly ignored, returns NULL on failure * @return array|bool|null */ - function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) { + function indexInfo( $table, $index, $fname = __METHOD__ ) { # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the # returned value except to check for the existance of indexes. $sql = "sp_helpindex '" . $table . "'"; $res = $this->query( $sql, $fname ); if ( !$res ) { - return NULL; + return null; } $result = array(); @@ -380,9 +394,14 @@ class DatabaseMssql extends DatabaseBase { * * Usually aborts on failure * If errors are explicitly ignored, returns success + * @param string $table + * @param array $arrToInsert + * @param string $fname + * @param array $options + * @throws DBQueryError * @return bool */ - function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) { + function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) { # No rows to insert, easy just return now if ( !count( $arrToInsert ) ) { return true; @@ -404,7 +423,7 @@ class DatabaseMssql extends DatabaseBase { $identity = null; $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" ); - if( $res && $res->numrows() ){ + if ( $res && $res->numrows() ) { // There is an identity for this table. $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) ); } @@ -417,13 +436,13 @@ class DatabaseMssql extends DatabaseBase { $identityClause = ''; // if we have an identity column - if( $identity ) { + if ( $identity ) { // iterate through - foreach ($a as $k => $v ) { + foreach ( $a as $k => $v ) { if ( $k == $identity ) { - if( !is_null($v) ){ + if ( !is_null( $v ) ) { // there is a value being passed to us, we need to turn on and off inserted identity - $sqlPre = "SET IDENTITY_INSERT $table ON;" ; + $sqlPre = "SET IDENTITY_INSERT $table ON;"; $sqlPost = ";SET IDENTITY_INSERT $table OFF;"; } else { @@ -474,7 +493,7 @@ class DatabaseMssql extends DatabaseBase { } elseif ( is_array( $value ) || is_object( $value ) ) { if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) { $sql .= $this->addQuotes( $value ); - } else { + } else { $sql .= $this->addQuotes( serialize( $value ) ); } } else { @@ -488,10 +507,10 @@ class DatabaseMssql extends DatabaseBase { if ( $ret === false ) { throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname ); - } elseif ( $ret != NULL ) { + } elseif ( $ret != null ) { // remember number of rows affected $this->mAffectedRows = sqlsrv_rows_affected( $ret ); - if ( !is_null($identity) ) { + if ( !is_null( $identity ) ) { // then we want to get the identity column value we were assigned and save it off $row = sqlsrv_fetch_object( $ret ); $this->mInsertId = $row->$identity; @@ -510,20 +529,28 @@ class DatabaseMssql extends DatabaseBase { * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() * $conds may be "*" to copy the whole table * srcTable may be an array of tables. - * @return null|\ResultWrapper + * @param string $destTable + * @param array|string $srcTable + * @param array $varMap + * @param array $conds + * @param string $fname + * @param array $insertOptions + * @param array $selectOptions + * @throws DBQueryError + * @return null|ResultWrapper */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect', + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, $insertOptions = array(), $selectOptions = array() ) { $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions ); if ( $ret === false ) { throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), /*$sql*/ '', $fname ); - } elseif ( $ret != NULL ) { + } elseif ( $ret != null ) { // remember number of rows affected $this->mAffectedRows = sqlsrv_rows_affected( $ret ); return $ret; } - return NULL; + return null; } /** @@ -590,9 +617,9 @@ class DatabaseMssql extends DatabaseBase { } else { $sql = ' SELECT * FROM ( - SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM ( - SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1 - ) as sub2 + SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM ( + SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1 + ) as sub2 ) AS sub3 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit ); return $sql; @@ -627,8 +654,8 @@ class DatabaseMssql extends DatabaseBase { /** * @return string wikitext of a link to the server software's web site */ - public static function getSoftwareLink() { - return "[http://www.microsoft.com/sql/ MS SQL Server]"; + public function getSoftwareLink() { + return "[{{int:version-db-mssql-url}} MS SQL Server]"; } /** @@ -643,11 +670,11 @@ class DatabaseMssql extends DatabaseBase { return $version; } - function tableExists ( $table, $fname = __METHOD__, $schema = false ) { + function tableExists( $table, $fname = __METHOD__, $schema = false ) { $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_name = '$table'" ); if ( $res === false ) { - print( "Error in tableExists query: " . $this->getErrors() ); + print "Error in tableExists query: " . $this->getErrors(); return false; } if ( sqlsrv_fetch( $res ) ) { @@ -661,12 +688,12 @@ class DatabaseMssql extends DatabaseBase { * Query whether a given column exists in the mediawiki schema * @return bool */ - function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) { + function fieldExists( $table, $field, $fname = __METHOD__ ) { $table = $this->tableName( $table ); $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); if ( $res === false ) { - print( "Error in fieldExists query: " . $this->getErrors() ); + print "Error in fieldExists query: " . $this->getErrors(); return false; } if ( sqlsrv_fetch( $res ) ) { @@ -681,7 +708,7 @@ class DatabaseMssql extends DatabaseBase { $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); if ( $res === false ) { - print( "Error in fieldInfo query: " . $this->getErrors() ); + print "Error in fieldInfo query: " . $this->getErrors(); return false; } $meta = $this->fetchRow( $res ); @@ -694,7 +721,7 @@ class DatabaseMssql extends DatabaseBase { /** * Begin a transaction, committing any previously open transaction */ - protected function doBegin( $fname = 'DatabaseMssql::begin' ) { + protected function doBegin( $fname = __METHOD__ ) { sqlsrv_begin_transaction( $this->mConn ); $this->mTrxLevel = 1; } @@ -702,7 +729,7 @@ class DatabaseMssql extends DatabaseBase { /** * End a transaction */ - protected function doCommit( $fname = 'DatabaseMssql::commit' ) { + protected function doCommit( $fname = __METHOD__ ) { sqlsrv_commit( $this->mConn ); $this->mTrxLevel = 0; } @@ -711,7 +738,7 @@ class DatabaseMssql extends DatabaseBase { * Rollback a transaction. * No-op on non-transactional databases. */ - protected function doRollback( $fname = 'DatabaseMssql::rollback' ) { + protected function doRollback( $fname = __METHOD__ ) { sqlsrv_rollback( $this->mConn ); $this->mTrxLevel = 0; } @@ -720,6 +747,8 @@ class DatabaseMssql extends DatabaseBase { * Escapes a identifier for use inm SQL. * Throws an exception if it is invalid. * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx + * @param $identifier + * @throws MWException * @return string */ private function escapeIdentifier( $identifier ) { @@ -750,17 +779,17 @@ class DatabaseMssql extends DatabaseBase { $newUser = $this->escapeIdentifier( $newUser ); $loginPassword = $this->addQuotes( $loginPassword ); - $this->doQuery("CREATE DATABASE $dbName;"); - $this->doQuery("USE $dbName;"); - $this->doQuery("CREATE SCHEMA $dbName;"); - $this->doQuery(" + $this->doQuery( "CREATE DATABASE $dbName;" ); + $this->doQuery( "USE $dbName;" ); + $this->doQuery( "CREATE SCHEMA $dbName;" ); + $this->doQuery( " CREATE LOGIN $newUser WITH PASSWORD=$loginPassword ; - "); - $this->doQuery(" + " ); + $this->doQuery( " CREATE USER $newUser FOR @@ -768,8 +797,8 @@ class DatabaseMssql extends DatabaseBase { WITH DEFAULT_SCHEMA=$dbName ; - "); - $this->doQuery(" + " ); + $this->doQuery( " GRANT BACKUP DATABASE, BACKUP LOG, @@ -784,17 +813,15 @@ class DatabaseMssql extends DatabaseBase { DATABASE::$dbName TO $newUser ; - "); - $this->doQuery(" + " ); + $this->doQuery( " GRANT CONTROL ON SCHEMA::$dbName TO $newUser ; - "); - - + " ); } function encodeBlob( $b ) { @@ -873,7 +900,7 @@ class DatabaseMssql extends DatabaseBase { /** * @private * - * @param $options Array: an associative array of options to be turned into + * @param array $options an associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return Array */ @@ -888,29 +915,23 @@ class DatabaseMssql extends DatabaseBase { } } - if ( isset( $options['GROUP BY'] ) ) { - $tailOpts .= " GROUP BY {$options['GROUP BY']}"; - } - if ( isset( $options['HAVING'] ) ) { - $tailOpts .= " HAVING {$options['GROUP BY']}"; - } - if ( isset( $options['ORDER BY'] ) ) { - $tailOpts .= " ORDER BY {$options['ORDER BY']}"; - } + $tailOpts .= $this->makeGroupByWithHaving( $options ); + + $tailOpts .= $this->makeOrderBy( $options ); if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) { $startOpts .= 'DISTINCT'; } // we want this to be compatible with the output of parent::makeSelectOptions() - return array( $startOpts, '' , $tailOpts, '' ); + return array( $startOpts, '', $tailOpts, '' ); } /** * Get the type of the DBMS, as it appears in $wgDBtype. * @return string */ - function getType(){ + function getType() { return 'mssql'; } @@ -940,7 +961,7 @@ class DatabaseMssql extends DatabaseBase { */ class MssqlField implements Field { private $name, $tablename, $default, $max_length, $nullable, $type; - function __construct ( $info ) { + function __construct( $info ) { $this->name = $info['COLUMN_NAME']; $this->tablename = $info['TABLE_NAME']; $this->default = $info['COLUMN_DEFAULT']; @@ -990,7 +1011,7 @@ class MssqlResult { $rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ); - foreach( $rows as $row ) { + foreach ( $rows as $row ) { if ( $row !== null ) { foreach ( $row as $k => $v ) { if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object @@ -1028,7 +1049,7 @@ class MssqlResult { $arrNum[] = $value; } } - switch( $mode ) { + switch ( $mode ) { case SQLSRV_FETCH_ASSOC: $ret = $this->mRows[$this->mCursor]; break; @@ -1081,43 +1102,101 @@ class MssqlResult { $i++; } // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table - switch( $intType ) { - case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break; - case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break; - case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break; - case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break; - case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break; - case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break; - case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break; - case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break; - case SQLSRV_SQLTYPE_INT: $strType = 'int'; break; - case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break; - case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break; - case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break; - case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break; - // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break; - case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break; - case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break; - case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break; - case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break; - case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break; - case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break; - case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break; - case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break; - case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break; - case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break; - case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break; - // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break; - case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break; - // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break; - case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break; - default: $strType = $intType; + switch ( $intType ) { + case SQLSRV_SQLTYPE_BIGINT: + $strType = 'bigint'; + break; + case SQLSRV_SQLTYPE_BINARY: + $strType = 'binary'; + break; + case SQLSRV_SQLTYPE_BIT: + $strType = 'bit'; + break; + case SQLSRV_SQLTYPE_CHAR: + $strType = 'char'; + break; + case SQLSRV_SQLTYPE_DATETIME: + $strType = 'datetime'; + break; + case SQLSRV_SQLTYPE_DECIMAL: // ($precision, $scale) + $strType = 'decimal'; + break; + case SQLSRV_SQLTYPE_FLOAT: + $strType = 'float'; + break; + case SQLSRV_SQLTYPE_IMAGE: + $strType = 'image'; + break; + case SQLSRV_SQLTYPE_INT: + $strType = 'int'; + break; + case SQLSRV_SQLTYPE_MONEY: + $strType = 'money'; + break; + case SQLSRV_SQLTYPE_NCHAR: // ($charCount): + $strType = 'nchar'; + break; + case SQLSRV_SQLTYPE_NUMERIC: // ($precision, $scale): + $strType = 'numeric'; + break; + case SQLSRV_SQLTYPE_NVARCHAR: // ($charCount) + $strType = 'nvarchar'; + break; + // case SQLSRV_SQLTYPE_NVARCHAR('max'): + // $strType = 'nvarchar(MAX)'; + // break; + case SQLSRV_SQLTYPE_NTEXT: + $strType = 'ntext'; + break; + case SQLSRV_SQLTYPE_REAL: + $strType = 'real'; + break; + case SQLSRV_SQLTYPE_SMALLDATETIME: + $strType = 'smalldatetime'; + break; + case SQLSRV_SQLTYPE_SMALLINT: + $strType = 'smallint'; + break; + case SQLSRV_SQLTYPE_SMALLMONEY: + $strType = 'smallmoney'; + break; + case SQLSRV_SQLTYPE_TEXT: + $strType = 'text'; + break; + case SQLSRV_SQLTYPE_TIMESTAMP: + $strType = 'timestamp'; + break; + case SQLSRV_SQLTYPE_TINYINT: + $strType = 'tinyint'; + break; + case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: + $strType = 'uniqueidentifier'; + break; + case SQLSRV_SQLTYPE_UDT: + $strType = 'UDT'; + break; + case SQLSRV_SQLTYPE_VARBINARY: // ($byteCount) + $strType = 'varbinary'; + break; + // case SQLSRV_SQLTYPE_VARBINARY('max'): + // $strType = 'varbinary(MAX)'; + // break; + case SQLSRV_SQLTYPE_VARCHAR: // ($charCount) + $strType = 'varchar'; + break; + // case SQLSRV_SQLTYPE_VARCHAR('max'): + // $strType = 'varchar(MAX)'; + // break; + case SQLSRV_SQLTYPE_XML: + $strType = 'xml'; + break; + default: + $strType = $intType; } return $strType; } public function free() { unset( $this->mRows ); - return; } } diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index 7f389da9..956bb694 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -22,27 +22,19 @@ */ /** - * Database abstraction object for mySQL - * Inherit all methods and properties of Database::Database() + * Database abstraction object for PHP extension mysql. * * @ingroup Database * @see Database */ -class DatabaseMysql extends DatabaseBase { - - /** - * @return string - */ - function getType() { - return 'mysql'; - } +class DatabaseMysql extends DatabaseMysqlBase { /** * @param $sql string * @return resource */ protected function doQuery( $sql ) { - if( $this->bufferResults() ) { + if ( $this->bufferResults() ) { $ret = mysql_query( $sql, $this->mConn ); } else { $ret = mysql_unbuffered_query( $sql, $this->mConn ); @@ -50,39 +42,13 @@ class DatabaseMysql extends DatabaseBase { return $ret; } - /** - * @param $server string - * @param $user string - * @param $password string - * @param $dbName string - * @return bool - * @throws DBConnectionError - */ - function open( $server, $user, $password, $dbName ) { - global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode; - wfProfileIn( __METHOD__ ); - - # Load mysql.so if we don't have it - wfDl( 'mysql' ); - + protected function mysqlConnect( $realServer ) { # Fail now # Otherwise we get a suppressed fatal error, which is very hard to track down - if ( !function_exists( 'mysql_connect' ) ) { + if ( !extension_loaded( 'mysql' ) ) { throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" ); } - # Debugging hack -- fake cluster - if ( $wgAllDBsAreLocalhost ) { - $realServer = 'localhost'; - } else { - $realServer = $server; - } - $this->close(); - $this->mServer = $server; - $this->mUser = $user; - $this->mPassword = $password; - $this->mDBname = $dbName; - $connFlags = 0; if ( $this->mFlags & DBO_SSL ) { $connFlags |= MYSQL_CLIENT_SSL; @@ -91,81 +57,27 @@ class DatabaseMysql extends DatabaseBase { $connFlags |= MYSQL_CLIENT_COMPRESS; } - wfProfileIn( "dbconnect-$server" ); - - # The kernel's default SYN retransmission period is far too slow for us, - # so we use a short timeout plus a manual retry. Retrying means that a small - # but finite rate of SYN packet loss won't cause user-visible errors. - $this->mConn = false; if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) { $numAttempts = 2; } else { $numAttempts = 1; } - $this->installErrorHandler(); - for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) { + + $conn = false; + + for ( $i = 0; $i < $numAttempts && !$conn; $i++ ) { if ( $i > 1 ) { usleep( 1000 ); } if ( $this->mFlags & DBO_PERSISTENT ) { - $this->mConn = mysql_pconnect( $realServer, $user, $password, $connFlags ); + $conn = mysql_pconnect( $realServer, $this->mUser, $this->mPassword, $connFlags ); } else { # Create a new connection... - $this->mConn = mysql_connect( $realServer, $user, $password, true, $connFlags ); - } - #if ( $this->mConn === false ) { - #$iplus = $i + 1; - #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); - #} - } - $error = $this->restoreErrorHandler(); - - wfProfileOut( "dbconnect-$server" ); - - # Always log connection errors - if ( !$this->mConn ) { - if ( !$error ) { - $error = $this->lastError(); - } - wfLogDBError( "Error connecting to {$this->mServer}: $error\n" ); - wfDebug( "DB connection error\n" . - "Server: $server, User: $user, Password: " . - substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); - - wfProfileOut( __METHOD__ ); - $this->reportConnectionError( $error ); - } - - if ( $dbName != '' ) { - wfSuppressWarnings(); - $success = mysql_select_db( $dbName, $this->mConn ); - wfRestoreWarnings(); - if ( !$success ) { - wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" ); - wfDebug( "Error selecting database $dbName on server {$this->mServer} " . - "from client host " . wfHostname() . "\n" ); - - wfProfileOut( __METHOD__ ); - $this->reportConnectionError( "Error selecting database $dbName" ); + $conn = mysql_connect( $realServer, $this->mUser, $this->mPassword, true, $connFlags ); } } - // Tell the server we're communicating with it in UTF-8. - // This may engage various charset conversions. - if( $wgDBmysql5 ) { - $this->query( 'SET NAMES utf8', __METHOD__ ); - } else { - $this->query( 'SET NAMES binary', __METHOD__ ); - } - // Set SQL mode, default is turning them all off, can be overridden or skipped with null - if ( is_string( $wgSQLMode ) ) { - $mode = $this->addQuotes( $wgSQLMode ); - $this->query( "SET sql_mode = $mode", __METHOD__ ); - } - - $this->mOpened = true; - wfProfileOut( __METHOD__ ); - return true; + return $conn; } /** @@ -176,111 +88,6 @@ class DatabaseMysql extends DatabaseBase { } /** - * @param $res ResultWrapper - * @throws DBUnexpectedError - */ - function freeResult( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - wfSuppressWarnings(); - $ok = mysql_free_result( $res ); - wfRestoreWarnings(); - if ( !$ok ) { - throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); - } - } - - /** - * @param $res ResultWrapper - * @return object|stdClass - * @throws DBUnexpectedError - */ - function fetchObject( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - wfSuppressWarnings(); - $row = mysql_fetch_object( $res ); - wfRestoreWarnings(); - - $errno = $this->lastErrno(); - // Unfortunately, mysql_fetch_object does not reset the last errno. - // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as - // these are the only errors mysql_fetch_object can cause. - // See http://dev.mysql.com/doc/refman/5.0/es/mysql-fetch-row.html. - if( $errno == 2000 || $errno == 2013 ) { - throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); - } - return $row; - } - - /** - * @param $res ResultWrapper - * @return array - * @throws DBUnexpectedError - */ - function fetchRow( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - wfSuppressWarnings(); - $row = mysql_fetch_array( $res ); - wfRestoreWarnings(); - - $errno = $this->lastErrno(); - // Unfortunately, mysql_fetch_array does not reset the last errno. - // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as - // these are the only errors mysql_fetch_object can cause. - // See http://dev.mysql.com/doc/refman/5.0/es/mysql-fetch-row.html. - if( $errno == 2000 || $errno == 2013 ) { - throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); - } - return $row; - } - - /** - * @throws DBUnexpectedError - * @param $res ResultWrapper - * @return int - */ - function numRows( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - wfSuppressWarnings(); - $n = mysql_num_rows( $res ); - wfRestoreWarnings(); - if( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) ); - } - return $n; - } - - /** - * @param $res ResultWrapper - * @return int - */ - function numFields( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - return mysql_num_fields( $res ); - } - - /** - * @param $res ResultWrapper - * @param $n string - * @return string - */ - function fieldName( $res, $n ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - return mysql_field_name( $res, $n ); - } - - /** * @return int */ function insertId() { @@ -288,18 +95,6 @@ class DatabaseMysql extends DatabaseBase { } /** - * @param $res ResultWrapper - * @param $row - * @return bool - */ - function dataSeek( $res, $row ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - return mysql_data_seek( $res, $row ); - } - - /** * @return int */ function lastErrno() { @@ -311,27 +106,6 @@ class DatabaseMysql extends DatabaseBase { } /** - * @return string - */ - function lastError() { - if ( $this->mConn ) { - # Even if it's non-zero, it can still be invalid - wfSuppressWarnings(); - $error = mysql_error( $this->mConn ); - if ( !$error ) { - $error = mysql_error(); - } - wfRestoreWarnings(); - } else { - $error = mysql_error(); - } - if( $error ) { - $error .= ' (' . $this->mServer . ')'; - } - return $error; - } - - /** * @return int */ function affectedRows() { @@ -339,100 +113,6 @@ class DatabaseMysql extends DatabaseBase { } /** - * @param $table string - * @param $uniqueIndexes - * @param $rows array - * @param $fname string - * @return ResultWrapper - */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMysql::replace' ) { - return $this->nativeReplace( $table, $rows, $fname ); - } - - /** - * Estimate rows in dataset - * Returns estimated count, based on EXPLAIN output - * Takes same arguments as Database::select() - * - * @param $table string|array - * @param $vars string|array - * @param $conds string|array - * @param $fname string - * @param $options string|array - * @return int - */ - public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'DatabaseMysql::estimateRowCount', $options = array() ) { - $options['EXPLAIN'] = true; - $res = $this->select( $table, $vars, $conds, $fname, $options ); - if ( $res === false ) { - return false; - } - if ( !$this->numRows( $res ) ) { - return 0; - } - - $rows = 1; - foreach ( $res as $plan ) { - $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero - } - return $rows; - } - - /** - * @param $table string - * @param $field string - * @return bool|MySQLField - */ - function fieldInfo( $table, $field ) { - $table = $this->tableName( $table ); - $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true ); - if ( !$res ) { - return false; - } - $n = mysql_num_fields( $res->result ); - for( $i = 0; $i < $n; $i++ ) { - $meta = mysql_fetch_field( $res->result, $i ); - if( $field == $meta->name ) { - return new MySQLField($meta); - } - } - return false; - } - - /** - * Get information about an index into an object - * Returns false if the index does not exist - * - * @param $table string - * @param $index string - * @param $fname string - * @return bool|array|null False or null on failure - */ - function indexInfo( $table, $index, $fname = 'DatabaseMysql::indexInfo' ) { - # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not. - # SHOW INDEX should work for 3.x and up: - # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html - $table = $this->tableName( $table ); - $index = $this->indexName( $index ); - $sql = 'SHOW INDEX FROM ' . $table; - $res = $this->query( $sql, $fname ); - - if ( !$res ) { - return null; - } - - $result = array(); - - foreach ( $res as $row ) { - if ( $row->Key_name == $index ) { - $result[] = $row; - } - } - - return empty( $result ) ? false : $result; - } - - /** * @param $db * @return bool */ @@ -442,599 +122,53 @@ class DatabaseMysql extends DatabaseBase { } /** - * @param $s string - * - * @return string - */ - function strencode( $s ) { - $sQuoted = mysql_real_escape_string( $s, $this->mConn ); - - if($sQuoted === false) { - $this->ping(); - $sQuoted = mysql_real_escape_string( $s, $this->mConn ); - } - return $sQuoted; - } - - /** - * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes". - * - * @param $s string - * - * @return string - */ - public function addIdentifierQuotes( $s ) { - return "`" . $this->strencode( $s ) . "`"; - } - - /** - * @param $name string - * @return bool - */ - public function isQuotedIdentifier( $name ) { - return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`'; - } - - /** - * @return bool - */ - function ping() { - $ping = mysql_ping( $this->mConn ); - if ( $ping ) { - return true; - } - - mysql_close( $this->mConn ); - $this->mOpened = false; - $this->mConn = false; - $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname ); - return true; - } - - /** - * Returns slave lag. - * - * This will do a SHOW SLAVE STATUS - * - * @return int - */ - function getLag() { - if ( !is_null( $this->mFakeSlaveLag ) ) { - wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" ); - return $this->mFakeSlaveLag; - } - - return $this->getLagFromSlaveStatus(); - } - - /** - * @return bool|int - */ - function getLagFromSlaveStatus() { - $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ ); - if ( !$res ) { - return false; - } - $row = $res->fetchObject(); - if ( !$row ) { - return false; - } - if ( strval( $row->Seconds_Behind_Master ) === '' ) { - return false; - } else { - return intval( $row->Seconds_Behind_Master ); - } - } - - /** - * @deprecated in 1.19, use getLagFromSlaveStatus - * - * @return bool|int - */ - function getLagFromProcesslist() { - wfDeprecated( __METHOD__, '1.19' ); - $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ ); - if( !$res ) { - return false; - } - # Find slave SQL thread - foreach( $res as $row ) { - /* This should work for most situations - when default db - * for thread is not specified, it had no events executed, - * and therefore it doesn't know yet how lagged it is. - * - * Relay log I/O thread does not select databases. - */ - if ( $row->User == 'system user' && - $row->State != 'Waiting for master to send event' && - $row->State != 'Connecting to master' && - $row->State != 'Queueing master event to the relay log' && - $row->State != 'Waiting for master update' && - $row->State != 'Requesting binlog dump' && - $row->State != 'Waiting to reconnect after a failed master event read' && - $row->State != 'Reconnecting after a failed master event read' && - $row->State != 'Registering slave on master' - ) { - # This is it, return the time (except -ve) - if ( $row->Time > 0x7fffffff ) { - return false; - } else { - return $row->Time; - } - } - } - return false; - } - - /** - * Wait for the slave to catch up to a given master position. - * - * @param $pos DBMasterPos object - * @param $timeout Integer: the maximum number of seconds to wait for synchronisation - * @return bool|string - */ - function masterPosWait( DBMasterPos $pos, $timeout ) { - $fname = 'DatabaseBase::masterPosWait'; - wfProfileIn( $fname ); - - # Commit any open transactions - if ( $this->mTrxLevel ) { - $this->commit( __METHOD__ ); - } - - if ( !is_null( $this->mFakeSlaveLag ) ) { - $status = parent::masterPosWait( $pos, $timeout ); - wfProfileOut( $fname ); - return $status; - } - - # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set - $encFile = $this->addQuotes( $pos->file ); - $encPos = intval( $pos->pos ); - $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)"; - $res = $this->doQuery( $sql ); - - if ( $res && $row = $this->fetchRow( $res ) ) { - wfProfileOut( $fname ); - return $row[0]; - } else { - wfProfileOut( $fname ); - return false; - } - } - - /** - * Get the position of the master from SHOW SLAVE STATUS - * - * @return MySQLMasterPos|bool - */ - function getSlavePos() { - if ( !is_null( $this->mFakeSlaveLag ) ) { - return parent::getSlavePos(); - } - - $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' ); - $row = $this->fetchObject( $res ); - - if ( $row ) { - $pos = isset( $row->Exec_master_log_pos ) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos; - return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos ); - } else { - return false; - } - } - - /** - * Get the position of the master from SHOW MASTER STATUS - * - * @return MySQLMasterPos|bool - */ - function getMasterPos() { - if ( $this->mFakeMaster ) { - return parent::getMasterPos(); - } - - $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' ); - $row = $this->fetchObject( $res ); - - if ( $row ) { - return new MySQLMasterPos( $row->File, $row->Position ); - } else { - return false; - } - } - - /** * @return string */ function getServerVersion() { return mysql_get_server_info( $this->mConn ); } - /** - * @param $index - * @return string - */ - function useIndexClause( $index ) { - return "FORCE INDEX (" . $this->indexName( $index ) . ")"; - } - - /** - * @return string - */ - function lowPriorityOption() { - return 'LOW_PRIORITY'; - } - - /** - * @return string - */ - public static function getSoftwareLink() { - return '[http://www.mysql.com/ MySQL]'; - } - - /** - * @param $options array - */ - public function setSessionOptions( array $options ) { - if ( isset( $options['connTimeout'] ) ) { - $timeout = (int)$options['connTimeout']; - $this->query( "SET net_read_timeout=$timeout" ); - $this->query( "SET net_write_timeout=$timeout" ); - } - } - - public function streamStatementEnd( &$sql, &$newLine ) { - if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) { - preg_match( '/^DELIMITER\s+(\S+)/' , $newLine, $m ); - $this->delimiter = $m[1]; - $newLine = ''; - } - return parent::streamStatementEnd( $sql, $newLine ); - } - - /** - * Check to see if a named lock is available. This is non-blocking. - * - * @param $lockName String: name of lock to poll - * @param $method String: name of method calling us - * @return Boolean - * @since 1.20 - */ - public function lockIsFree( $lockName, $method ) { - $lockName = $this->addQuotes( $lockName ); - $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method ); - $row = $this->fetchObject( $result ); - return ( $row->lockstatus == 1 ); - } - - /** - * @param $lockName string - * @param $method string - * @param $timeout int - * @return bool - */ - public function lock( $lockName, $method, $timeout = 5 ) { - $lockName = $this->addQuotes( $lockName ); - $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method ); - $row = $this->fetchObject( $result ); - - if( $row->lockstatus == 1 ) { - return true; - } else { - wfDebug( __METHOD__." failed to acquire lock\n" ); - return false; - } - } - - /** - * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock - * @param $lockName string - * @param $method string - * @return bool - */ - public function unlock( $lockName, $method ) { - $lockName = $this->addQuotes( $lockName ); - $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method ); - $row = $this->fetchObject( $result ); - return ( $row->lockstatus == 1 ); - } - - /** - * @param $read array - * @param $write array - * @param $method string - * @param $lowPriority bool - */ - public function lockTables( $read, $write, $method, $lowPriority = true ) { - $items = array(); - - foreach( $write as $table ) { - $tbl = $this->tableName( $table ) . - ( $lowPriority ? ' LOW_PRIORITY' : '' ) . - ' WRITE'; - $items[] = $tbl; - } - foreach( $read as $table ) { - $items[] = $this->tableName( $table ) . ' READ'; - } - $sql = "LOCK TABLES " . implode( ',', $items ); - $this->query( $sql, $method ); - } - - /** - * @param $method string - */ - public function unlockTables( $method ) { - $this->query( "UNLOCK TABLES", $method ); - } - - /** - * Get search engine class. All subclasses of this - * need to implement this if they wish to use searching. - * - * @return String - */ - public function getSearchEngine() { - return 'SearchMySQL'; + protected function mysqlFreeResult( $res ) { + return mysql_free_result( $res ); } - /** - * @param bool $value - * @return mixed - */ - public function setBigSelects( $value = true ) { - if ( $value === 'default' ) { - if ( $this->mDefaultBigSelects === null ) { - # Function hasn't been called before so it must already be set to the default - return; - } else { - $value = $this->mDefaultBigSelects; - } - } elseif ( $this->mDefaultBigSelects === null ) { - $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects' ); - } - $encValue = $value ? '1' : '0'; - $this->query( "SET sql_big_selects=$encValue", __METHOD__ ); + protected function mysqlFetchObject( $res ) { + return mysql_fetch_object( $res ); } - /** - * DELETE where the condition is a join. MySql uses multi-table deletes. - * @param $delTable string - * @param $joinTable string - * @param $delVar string - * @param $joinVar string - * @param $conds array|string - * @param $fname bool - * @return bool|ResultWrapper - */ - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) { - if ( !$conds ) { - throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' ); - } - - $delTable = $this->tableName( $delTable ); - $joinTable = $this->tableName( $joinTable ); - $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar "; - - if ( $conds != '*' ) { - $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); - } - - return $this->query( $sql, $fname ); + protected function mysqlFetchArray( $res ) { + return mysql_fetch_array( $res ); } - /** - * Determines how long the server has been up - * - * @return int - */ - function getServerUptime() { - $vars = $this->getMysqlStatus( 'Uptime' ); - return (int)$vars['Uptime']; + protected function mysqlNumRows( $res ) { + return mysql_num_rows( $res ); } - /** - * Determines if the last failure was due to a deadlock - * - * @return bool - */ - function wasDeadlock() { - return $this->lastErrno() == 1213; - } - - /** - * Determines if the last failure was due to a lock timeout - * - * @return bool - */ - function wasLockTimeout() { - return $this->lastErrno() == 1205; - } - - /** - * Determines if the last query error was something that should be dealt - * with by pinging the connection and reissuing the query - * - * @return bool - */ - function wasErrorReissuable() { - return $this->lastErrno() == 2013 || $this->lastErrno() == 2006; - } - - /** - * Determines if the last failure was due to the database being read-only. - * - * @return bool - */ - function wasReadOnlyError() { - return $this->lastErrno() == 1223 || - ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false ); - } - - /** - * @param $oldName - * @param $newName - * @param $temporary bool - * @param $fname string - */ - function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseMysql::duplicateTableStructure' ) { - $tmp = $temporary ? 'TEMPORARY ' : ''; - $newName = $this->addIdentifierQuotes( $newName ); - $oldName = $this->addIdentifierQuotes( $oldName ); - $query = "CREATE $tmp TABLE $newName (LIKE $oldName)"; - $this->query( $query, $fname ); - } - - /** - * List all tables on the database - * - * @param $prefix string Only show tables with this prefix, e.g. mw_ - * @param $fname String: calling function name - * @return array - */ - function listTables( $prefix = null, $fname = 'DatabaseMysql::listTables' ) { - $result = $this->query( "SHOW TABLES", $fname); - - $endArray = array(); - - foreach( $result as $table ) { - $vars = get_object_vars($table); - $table = array_pop( $vars ); - - if( !$prefix || strpos( $table, $prefix ) === 0 ) { - $endArray[] = $table; - } - } - - return $endArray; - } - - /** - * @param $tableName - * @param $fName string - * @return bool|ResultWrapper - */ - public function dropTable( $tableName, $fName = 'DatabaseMysql::dropTable' ) { - if( !$this->tableExists( $tableName, $fName ) ) { - return false; - } - return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName ); - } - - /** - * @return array - */ - protected function getDefaultSchemaVars() { - $vars = parent::getDefaultSchemaVars(); - $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] ); - $vars['wgDBTableOptions'] = str_replace( 'CHARSET=mysql4', 'CHARSET=binary', $vars['wgDBTableOptions'] ); - return $vars; - } - - /** - * Get status information from SHOW STATUS in an associative array - * - * @param $which string - * @return array - */ - function getMysqlStatus( $which = "%" ) { - $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); - $status = array(); - - foreach ( $res as $row ) { - $status[$row->Variable_name] = $row->Value; - } - - return $status; - } - -} - -/** - * Legacy support: Database == DatabaseMysql - * - * @deprecated in 1.16 - */ -class Database extends DatabaseMysql {} - -/** - * Utility class. - * @ingroup Database - */ -class MySQLField implements Field { - private $name, $tablename, $default, $max_length, $nullable, - $is_pk, $is_unique, $is_multiple, $is_key, $type; - - 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; - } - - /** - * @return string - */ - function name() { - return $this->name; - } - - /** - * @return string - */ - function tableName() { - return $this->tableName; - } - - /** - * @return string - */ - function type() { - return $this->type; + protected function mysqlNumFields( $res ) { + return mysql_num_fields( $res ); } - /** - * @return bool - */ - function isNullable() { - return $this->nullable; + protected function mysqlFetchField( $res, $n ) { + return mysql_fetch_field( $res, $n ); } - function defaultValue() { - return $this->default; + protected function mysqlFieldName( $res, $n ) { + return mysql_field_name( $res, $n ); } - /** - * @return bool - */ - function isKey() { - return $this->is_key; + protected function mysqlDataSeek( $res, $row ) { + return mysql_data_seek( $res, $row ); } - /** - * @return bool - */ - function isMultipleKey() { - return $this->is_multiple; + protected function mysqlError( $conn = null ) { + return ( $conn !== null ) ? mysql_error( $conn ) : mysql_error(); // avoid warning } -} - -class MySQLMasterPos implements DBMasterPos { - var $file, $pos; - function __construct( $file, $pos ) { - $this->file = $file; - $this->pos = $pos; + protected function mysqlRealEscapeString( $s ) { + return mysql_real_escape_string( $s, $this->mConn ); } - function __toString() { - return "{$this->file}/{$this->pos}"; + protected function mysqlPing() { + return mysql_ping( $this->mConn ); } } diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php new file mode 100644 index 00000000..8f12b92d --- /dev/null +++ b/includes/db/DatabaseMysqlBase.php @@ -0,0 +1,1161 @@ +<?php +/** + * This is the MySQL database abstraction layer. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Database + */ + +/** + * Database abstraction object for MySQL. + * Defines methods independent on used MySQL extension. + * + * @ingroup Database + * @since 1.22 + * @see Database + */ +abstract class DatabaseMysqlBase extends DatabaseBase { + /** @var MysqlMasterPos */ + protected $lastKnownSlavePos; + + /** + * @return string + */ + function getType() { + return 'mysql'; + } + + /** + * @param $server string + * @param $user string + * @param $password string + * @param $dbName string + * @return bool + * @throws DBConnectionError + */ + function open( $server, $user, $password, $dbName ) { + global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode; + wfProfileIn( __METHOD__ ); + + # Debugging hack -- fake cluster + if ( $wgAllDBsAreLocalhost ) { + $realServer = 'localhost'; + } else { + $realServer = $server; + } + $this->close(); + $this->mServer = $server; + $this->mUser = $user; + $this->mPassword = $password; + $this->mDBname = $dbName; + + wfProfileIn( "dbconnect-$server" ); + + # The kernel's default SYN retransmission period is far too slow for us, + # so we use a short timeout plus a manual retry. Retrying means that a small + # but finite rate of SYN packet loss won't cause user-visible errors. + $this->mConn = false; + $this->installErrorHandler(); + try { + $this->mConn = $this->mysqlConnect( $realServer ); + } catch ( Exception $ex ) { + wfProfileOut( "dbconnect-$server" ); + wfProfileOut( __METHOD__ ); + throw $ex; + } + $error = $this->restoreErrorHandler(); + + wfProfileOut( "dbconnect-$server" ); + + # Always log connection errors + if ( !$this->mConn ) { + if ( !$error ) { + $error = $this->lastError(); + } + wfLogDBError( "Error connecting to {$this->mServer}: $error\n" ); + wfDebug( "DB connection error\n" . + "Server: $server, User: $user, Password: " . + substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); + + wfProfileOut( __METHOD__ ); + return $this->reportConnectionError( $error ); + } + + if ( $dbName != '' ) { + wfSuppressWarnings(); + $success = $this->selectDB( $dbName ); + wfRestoreWarnings(); + if ( !$success ) { + wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" ); + wfDebug( "Error selecting database $dbName on server {$this->mServer} " . + "from client host " . wfHostname() . "\n" ); + + wfProfileOut( __METHOD__ ); + return $this->reportConnectionError( "Error selecting database $dbName" ); + } + } + + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + if ( $wgDBmysql5 ) { + $this->query( 'SET NAMES utf8', __METHOD__ ); + } else { + $this->query( 'SET NAMES binary', __METHOD__ ); + } + // Set SQL mode, default is turning them all off, can be overridden or skipped with null + if ( is_string( $wgSQLMode ) ) { + $mode = $this->addQuotes( $wgSQLMode ); + $this->query( "SET sql_mode = $mode", __METHOD__ ); + } + + $this->mOpened = true; + wfProfileOut( __METHOD__ ); + return true; + } + + /** + * Open a connection to a MySQL server + * + * @param $realServer string + * @return mixed Raw connection + * @throws DBConnectionError + */ + abstract protected function mysqlConnect( $realServer ); + + /** + * @param $res ResultWrapper + * @throws DBUnexpectedError + */ + function freeResult( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + wfSuppressWarnings(); + $ok = $this->mysqlFreeResult( $res ); + wfRestoreWarnings(); + if ( !$ok ) { + throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); + } + } + + /** + * Free result memory + * + * @param $res Raw result + * @return bool + */ + abstract protected function mysqlFreeResult( $res ); + + /** + * @param $res ResultWrapper + * @return object|bool + * @throws DBUnexpectedError + */ + function fetchObject( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + wfSuppressWarnings(); + $row = $this->mysqlFetchObject( $res ); + wfRestoreWarnings(); + + $errno = $this->lastErrno(); + // Unfortunately, mysql_fetch_object does not reset the last errno. + // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as + // these are the only errors mysql_fetch_object can cause. + // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html. + if ( $errno == 2000 || $errno == 2013 ) { + throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); + } + return $row; + } + + /** + * Fetch a result row as an object + * + * @param $res Raw result + * @return stdClass + */ + abstract protected function mysqlFetchObject( $res ); + + /** + * @param $res ResultWrapper + * @return array|bool + * @throws DBUnexpectedError + */ + function fetchRow( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + wfSuppressWarnings(); + $row = $this->mysqlFetchArray( $res ); + wfRestoreWarnings(); + + $errno = $this->lastErrno(); + // Unfortunately, mysql_fetch_array does not reset the last errno. + // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as + // these are the only errors mysql_fetch_array can cause. + // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html. + if ( $errno == 2000 || $errno == 2013 ) { + throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); + } + return $row; + } + + /** + * Fetch a result row as an associative and numeric array + * + * @param $res Raw result + * @return array + */ + abstract protected function mysqlFetchArray( $res ); + + /** + * @throws DBUnexpectedError + * @param $res ResultWrapper + * @return int + */ + function numRows( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + wfSuppressWarnings(); + $n = $this->mysqlNumRows( $res ); + wfRestoreWarnings(); + // Unfortunately, mysql_num_rows does not reset the last errno. + // We are not checking for any errors here, since + // these are no errors mysql_num_rows can cause. + // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html. + // See https://bugzilla.wikimedia.org/42430 + return $n; + } + + /** + * Get number of rows in result + * + * @param $res Raw result + * @return int + */ + abstract protected function mysqlNumRows( $res ); + + /** + * @param $res ResultWrapper + * @return int + */ + function numFields( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return $this->mysqlNumFields( $res ); + } + + /** + * Get number of fields in result + * + * @param $res Raw result + * @return int + */ + abstract protected function mysqlNumFields( $res ); + + /** + * @param $res ResultWrapper + * @param $n string + * @return string + */ + function fieldName( $res, $n ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return $this->mysqlFieldName( $res, $n ); + } + + /** + * Get the name of the specified field in a result + * + * @param $res Raw result + * @param $n int + * @return string + */ + abstract protected function mysqlFieldName( $res, $n ); + + /** + * @param $res ResultWrapper + * @param $row + * @return bool + */ + function dataSeek( $res, $row ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return $this->mysqlDataSeek( $res, $row ); + } + + /** + * Move internal result pointer + * + * @param $res Raw result + * @param $row int + * @return bool + */ + abstract protected function mysqlDataSeek( $res, $row ); + + /** + * @return string + */ + function lastError() { + if ( $this->mConn ) { + # Even if it's non-zero, it can still be invalid + wfSuppressWarnings(); + $error = $this->mysqlError( $this->mConn ); + if ( !$error ) { + $error = $this->mysqlError(); + } + wfRestoreWarnings(); + } else { + $error = $this->mysqlError(); + } + if ( $error ) { + $error .= ' (' . $this->mServer . ')'; + } + return $error; + } + + /** + * Returns the text of the error message from previous MySQL operation + * + * @param $conn Raw connection + * @return string + */ + abstract protected function mysqlError( $conn = null ); + + /** + * @param $table string + * @param $uniqueIndexes + * @param $rows array + * @param $fname string + * @return ResultWrapper + */ + function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { + return $this->nativeReplace( $table, $rows, $fname ); + } + + /** + * Estimate rows in dataset + * Returns estimated count, based on EXPLAIN output + * Takes same arguments as Database::select() + * + * @param $table string|array + * @param $vars string|array + * @param $conds string|array + * @param $fname string + * @param $options string|array + * @return int + */ + public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) { + $options['EXPLAIN'] = true; + $res = $this->select( $table, $vars, $conds, $fname, $options ); + if ( $res === false ) { + return false; + } + if ( !$this->numRows( $res ) ) { + return 0; + } + + $rows = 1; + foreach ( $res as $plan ) { + $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero + } + return $rows; + } + + /** + * @param $table string + * @param $field string + * @return bool|MySQLField + */ + function fieldInfo( $table, $field ) { + $table = $this->tableName( $table ); + $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true ); + if ( !$res ) { + return false; + } + $n = $this->mysqlNumFields( $res->result ); + for ( $i = 0; $i < $n; $i++ ) { + $meta = $this->mysqlFetchField( $res->result, $i ); + if ( $field == $meta->name ) { + return new MySQLField( $meta ); + } + } + return false; + } + + /** + * Get column information from a result + * + * @param $res Raw result + * @param $n int + * @return stdClass + */ + abstract protected function mysqlFetchField( $res, $n ); + + /** + * Get information about an index into an object + * Returns false if the index does not exist + * + * @param $table string + * @param $index string + * @param $fname string + * @return bool|array|null False or null on failure + */ + function indexInfo( $table, $index, $fname = __METHOD__ ) { + # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not. + # SHOW INDEX should work for 3.x and up: + # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html + $table = $this->tableName( $table ); + $index = $this->indexName( $index ); + + $sql = 'SHOW INDEX FROM ' . $table; + $res = $this->query( $sql, $fname ); + + if ( !$res ) { + return null; + } + + $result = array(); + + foreach ( $res as $row ) { + if ( $row->Key_name == $index ) { + $result[] = $row; + } + } + return empty( $result ) ? false : $result; + } + + /** + * @param $s string + * + * @return string + */ + function strencode( $s ) { + $sQuoted = $this->mysqlRealEscapeString( $s ); + + if ( $sQuoted === false ) { + $this->ping(); + $sQuoted = $this->mysqlRealEscapeString( $s ); + } + return $sQuoted; + } + + /** + * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes". + * + * @param $s string + * + * @return string + */ + public function addIdentifierQuotes( $s ) { + // Characters in the range \u0001-\uFFFF are valid in a quoted identifier + // Remove NUL bytes and escape backticks by doubling + return '`' . str_replace( array( "\0", '`' ), array( '', '``' ), $s ) . '`'; + } + + /** + * @param $name string + * @return bool + */ + public function isQuotedIdentifier( $name ) { + return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`'; + } + + /** + * @return bool + */ + function ping() { + $ping = $this->mysqlPing(); + if ( $ping ) { + return true; + } + + $this->closeConnection(); + $this->mOpened = false; + $this->mConn = false; + $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname ); + return true; + } + + /** + * Ping a server connection or reconnect if there is no connection + * + * @return bool + */ + abstract protected function mysqlPing(); + + /** + * Returns slave lag. + * + * This will do a SHOW SLAVE STATUS + * + * @return int + */ + function getLag() { + if ( !is_null( $this->mFakeSlaveLag ) ) { + wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" ); + return $this->mFakeSlaveLag; + } + + return $this->getLagFromSlaveStatus(); + } + + /** + * @return bool|int + */ + function getLagFromSlaveStatus() { + $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ ); + if ( !$res ) { + return false; + } + $row = $res->fetchObject(); + if ( !$row ) { + return false; + } + if ( strval( $row->Seconds_Behind_Master ) === '' ) { + return false; + } else { + return intval( $row->Seconds_Behind_Master ); + } + } + + /** + * @deprecated in 1.19, use getLagFromSlaveStatus + * + * @return bool|int + */ + function getLagFromProcesslist() { + wfDeprecated( __METHOD__, '1.19' ); + $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ ); + if ( !$res ) { + return false; + } + # Find slave SQL thread + foreach ( $res as $row ) { + /* This should work for most situations - when default db + * for thread is not specified, it had no events executed, + * and therefore it doesn't know yet how lagged it is. + * + * Relay log I/O thread does not select databases. + */ + if ( $row->User == 'system user' && + $row->State != 'Waiting for master to send event' && + $row->State != 'Connecting to master' && + $row->State != 'Queueing master event to the relay log' && + $row->State != 'Waiting for master update' && + $row->State != 'Requesting binlog dump' && + $row->State != 'Waiting to reconnect after a failed master event read' && + $row->State != 'Reconnecting after a failed master event read' && + $row->State != 'Registering slave on master' + ) { + # This is it, return the time (except -ve) + if ( $row->Time > 0x7fffffff ) { + return false; + } else { + return $row->Time; + } + } + } + return false; + } + + /** + * Wait for the slave to catch up to a given master position. + * @TODO: return values for this and base class are rubbish + * + * @param $pos DBMasterPos object + * @param $timeout Integer: the maximum number of seconds to wait for synchronisation + * @return bool|string + */ + function masterPosWait( DBMasterPos $pos, $timeout ) { + if ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) { + return '0'; // http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html + } + + wfProfileIn( __METHOD__ ); + # Commit any open transactions + $this->commit( __METHOD__, 'flush' ); + + if ( !is_null( $this->mFakeSlaveLag ) ) { + $status = parent::masterPosWait( $pos, $timeout ); + wfProfileOut( __METHOD__ ); + return $status; + } + + # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set + $encFile = $this->addQuotes( $pos->file ); + $encPos = intval( $pos->pos ); + $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)"; + $res = $this->doQuery( $sql ); + + $status = false; + if ( $res && $row = $this->fetchRow( $res ) ) { + $status = $row[0]; // can be NULL, -1, or 0+ per the MySQL manual + if ( ctype_digit( $status ) ) { // success + $this->lastKnownSlavePos = $pos; + } + } + + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * Get the position of the master from SHOW SLAVE STATUS + * + * @return MySQLMasterPos|bool + */ + function getSlavePos() { + if ( !is_null( $this->mFakeSlaveLag ) ) { + return parent::getSlavePos(); + } + + $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' ); + $row = $this->fetchObject( $res ); + + if ( $row ) { + $pos = isset( $row->Exec_master_log_pos ) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos; + return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos ); + } else { + return false; + } + } + + /** + * Get the position of the master from SHOW MASTER STATUS + * + * @return MySQLMasterPos|bool + */ + function getMasterPos() { + if ( $this->mFakeMaster ) { + return parent::getMasterPos(); + } + + $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' ); + $row = $this->fetchObject( $res ); + + if ( $row ) { + return new MySQLMasterPos( $row->File, $row->Position ); + } else { + return false; + } + } + + /** + * @param $index + * @return string + */ + function useIndexClause( $index ) { + return "FORCE INDEX (" . $this->indexName( $index ) . ")"; + } + + /** + * @return string + */ + function lowPriorityOption() { + return 'LOW_PRIORITY'; + } + + /** + * @return string + */ + public function getSoftwareLink() { + $version = $this->getServerVersion(); + if ( strpos( $version, 'MariaDB' ) !== false ) { + return '[{{int:version-db-mariadb-url}} MariaDB]'; + } elseif ( strpos( $version, 'percona' ) !== false ) { + return '[{{int:version-db-percona-url}} Percona Server]'; + } else { + return '[{{int:version-db-mysql-url}} MySQL]'; + } + } + + /** + * @param $options array + */ + public function setSessionOptions( array $options ) { + if ( isset( $options['connTimeout'] ) ) { + $timeout = (int)$options['connTimeout']; + $this->query( "SET net_read_timeout=$timeout" ); + $this->query( "SET net_write_timeout=$timeout" ); + } + } + + public function streamStatementEnd( &$sql, &$newLine ) { + if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) { + preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m ); + $this->delimiter = $m[1]; + $newLine = ''; + } + return parent::streamStatementEnd( $sql, $newLine ); + } + + /** + * Check to see if a named lock is available. This is non-blocking. + * + * @param string $lockName name of lock to poll + * @param string $method name of method calling us + * @return Boolean + * @since 1.20 + */ + public function lockIsFree( $lockName, $method ) { + $lockName = $this->addQuotes( $lockName ); + $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method ); + $row = $this->fetchObject( $result ); + return ( $row->lockstatus == 1 ); + } + + /** + * @param $lockName string + * @param $method string + * @param $timeout int + * @return bool + */ + public function lock( $lockName, $method, $timeout = 5 ) { + $lockName = $this->addQuotes( $lockName ); + $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method ); + $row = $this->fetchObject( $result ); + + if ( $row->lockstatus == 1 ) { + return true; + } else { + wfDebug( __METHOD__ . " failed to acquire lock\n" ); + return false; + } + } + + /** + * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock + * @param $lockName string + * @param $method string + * @return bool + */ + public function unlock( $lockName, $method ) { + $lockName = $this->addQuotes( $lockName ); + $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method ); + $row = $this->fetchObject( $result ); + return ( $row->lockstatus == 1 ); + } + + /** + * @param $read array + * @param $write array + * @param $method string + * @param $lowPriority bool + * @return bool + */ + public function lockTables( $read, $write, $method, $lowPriority = true ) { + $items = array(); + + foreach ( $write as $table ) { + $tbl = $this->tableName( $table ) . + ( $lowPriority ? ' LOW_PRIORITY' : '' ) . + ' WRITE'; + $items[] = $tbl; + } + foreach ( $read as $table ) { + $items[] = $this->tableName( $table ) . ' READ'; + } + $sql = "LOCK TABLES " . implode( ',', $items ); + $this->query( $sql, $method ); + return true; + } + + /** + * @param $method string + * @return bool + */ + public function unlockTables( $method ) { + $this->query( "UNLOCK TABLES", $method ); + return true; + } + + /** + * Get search engine class. All subclasses of this + * need to implement this if they wish to use searching. + * + * @return String + */ + public function getSearchEngine() { + return 'SearchMySQL'; + } + + /** + * @param bool $value + * @return mixed + */ + public function setBigSelects( $value = true ) { + if ( $value === 'default' ) { + if ( $this->mDefaultBigSelects === null ) { + # Function hasn't been called before so it must already be set to the default + return; + } else { + $value = $this->mDefaultBigSelects; + } + } elseif ( $this->mDefaultBigSelects === null ) { + $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects' ); + } + $encValue = $value ? '1' : '0'; + $this->query( "SET sql_big_selects=$encValue", __METHOD__ ); + } + + /** + * DELETE where the condition is a join. MySql uses multi-table deletes. + * @param $delTable string + * @param $joinTable string + * @param $delVar string + * @param $joinVar string + * @param $conds array|string + * @param bool|string $fname bool + * @throws DBUnexpectedError + * @return bool|ResultWrapper + */ + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) { + if ( !$conds ) { + throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' ); + } + + $delTable = $this->tableName( $delTable ); + $joinTable = $this->tableName( $joinTable ); + $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar "; + + if ( $conds != '*' ) { + $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); + } + + return $this->query( $sql, $fname ); + } + + /** + * @param string $table + * @param array $rows + * @param array $uniqueIndexes + * @param array $set + * @param string $fname + * @param array $options + * @return bool + */ + public function upsert( + $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ + ) { + if ( !count( $rows ) ) { + return true; // nothing to do + } + $rows = is_array( reset( $rows ) ) ? $rows : array( $rows ); + + $table = $this->tableName( $table ); + $columns = array_keys( $rows[0] ); + + $sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES '; + $rowTuples = array(); + foreach ( $rows as $row ) { + $rowTuples[] = '(' . $this->makeList( $row ) . ')'; + } + $sql .= implode( ',', $rowTuples ); + $sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, LIST_SET ); + + return (bool)$this->query( $sql, $fname ); + } + + /** + * Determines how long the server has been up + * + * @return int + */ + function getServerUptime() { + $vars = $this->getMysqlStatus( 'Uptime' ); + return (int)$vars['Uptime']; + } + + /** + * Determines if the last failure was due to a deadlock + * + * @return bool + */ + function wasDeadlock() { + return $this->lastErrno() == 1213; + } + + /** + * Determines if the last failure was due to a lock timeout + * + * @return bool + */ + function wasLockTimeout() { + return $this->lastErrno() == 1205; + } + + /** + * Determines if the last query error was something that should be dealt + * with by pinging the connection and reissuing the query + * + * @return bool + */ + function wasErrorReissuable() { + return $this->lastErrno() == 2013 || $this->lastErrno() == 2006; + } + + /** + * Determines if the last failure was due to the database being read-only. + * + * @return bool + */ + function wasReadOnlyError() { + return $this->lastErrno() == 1223 || + ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false ); + } + + /** + * @param $oldName + * @param $newName + * @param $temporary bool + * @param $fname string + */ + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) { + $tmp = $temporary ? 'TEMPORARY ' : ''; + $newName = $this->addIdentifierQuotes( $newName ); + $oldName = $this->addIdentifierQuotes( $oldName ); + $query = "CREATE $tmp TABLE $newName (LIKE $oldName)"; + $this->query( $query, $fname ); + } + + /** + * List all tables on the database + * + * @param string $prefix Only show tables with this prefix, e.g. mw_ + * @param string $fname calling function name + * @return array + */ + function listTables( $prefix = null, $fname = __METHOD__ ) { + $result = $this->query( "SHOW TABLES", $fname ); + + $endArray = array(); + + foreach ( $result as $table ) { + $vars = get_object_vars( $table ); + $table = array_pop( $vars ); + + if ( !$prefix || strpos( $table, $prefix ) === 0 ) { + $endArray[] = $table; + } + } + + return $endArray; + } + + /** + * @param $tableName + * @param $fName string + * @return bool|ResultWrapper + */ + public function dropTable( $tableName, $fName = __METHOD__ ) { + if ( !$this->tableExists( $tableName, $fName ) ) { + return false; + } + return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName ); + } + + /** + * @return array + */ + protected function getDefaultSchemaVars() { + $vars = parent::getDefaultSchemaVars(); + $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] ); + $vars['wgDBTableOptions'] = str_replace( 'CHARSET=mysql4', 'CHARSET=binary', $vars['wgDBTableOptions'] ); + return $vars; + } + + /** + * Get status information from SHOW STATUS in an associative array + * + * @param $which string + * @return array + */ + function getMysqlStatus( $which = "%" ) { + $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); + $status = array(); + + foreach ( $res as $row ) { + $status[$row->Variable_name] = $row->Value; + } + + return $status; + } + + /** + * Lists VIEWs in the database + * + * @param string $prefix Only show VIEWs with this prefix, eg. + * unit_test_, or $wgDBprefix. Default: null, would return all views. + * @param string $fname Name of calling function + * @return array + * @since 1.22 + */ + public function listViews( $prefix = null, $fname = __METHOD__ ) { + + if ( !isset( $this->allViews ) ) { + + // The name of the column containing the name of the VIEW + $propertyName = 'Tables_in_' . $this->mDBname; + + // Query for the VIEWS + $result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' ); + $this->allViews = array(); + while ( ($row = $this->fetchRow($result)) !== false ) { + array_push( $this->allViews, $row[$propertyName] ); + } + } + + if ( is_null($prefix) || $prefix === '' ) { + return $this->allViews; + } + + $filteredViews = array(); + foreach ( $this->allViews as $viewName ) { + // Does the name of this VIEW start with the table-prefix? + if ( strpos( $viewName, $prefix ) === 0 ) { + array_push( $filteredViews, $viewName ); + } + } + return $filteredViews; + } + + /** + * Differentiates between a TABLE and a VIEW. + * + * @param $name string: Name of the TABLE/VIEW to test + * @return bool + * @since 1.22 + */ + public function isView( $name, $prefix = null ) { + return in_array( $name, $this->listViews( $prefix ) ); + } + +} + + + +/** + * 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; + + 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; + } + + /** + * @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; + } + + function isBinary() { + return $this->binary; + } +} + +class MySQLMasterPos implements DBMasterPos { + var $file, $pos; + + function __construct( $file, $pos ) { + $this->file = $file; + $this->pos = $pos; + } + + function __toString() { + // e.g db1034-bin.000976/843431247 + return "{$this->file}/{$this->pos}"; + } + + /** + * @return array|false (int, int) + */ + protected function getCoordinates() { + $m = array(); + if ( preg_match( '!\.(\d+)/(\d+)$!', (string)$this, $m ) ) { + return array( (int)$m[1], (int)$m[2] ); + } + return false; + } + + function hasReached( MySQLMasterPos $pos ) { + $thisPos = $this->getCoordinates(); + $thatPos = $pos->getCoordinates(); + return ( $thisPos && $thatPos && $thisPos >= $thatPos ); + } +} diff --git a/includes/db/DatabaseMysqli.php b/includes/db/DatabaseMysqli.php new file mode 100644 index 00000000..7761abe9 --- /dev/null +++ b/includes/db/DatabaseMysqli.php @@ -0,0 +1,194 @@ +<?php +/** + * This is the MySQLi database abstraction layer. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Database + */ + +/** + * Database abstraction object for PHP extension mysqli. + * + * @ingroup Database + * @since 1.22 + * @see Database + */ +class DatabaseMysqli extends DatabaseMysqlBase { + + /** + * @param $sql string + * @return resource + */ + protected function doQuery( $sql ) { + if ( $this->bufferResults() ) { + $ret = $this->mConn->query( $sql ); + } else { + $ret = $this->mConn->query( $sql, MYSQLI_USE_RESULT ); + } + return $ret; + } + + protected function mysqlConnect( $realServer ) { + # Fail now + # Otherwise we get a suppressed fatal error, which is very hard to track down + if ( !function_exists( 'mysqli_init' ) ) { + throw new DBConnectionError( $this, "MySQLi functions missing," + . " have you compiled PHP with the --with-mysqli option?\n" ); + } + + $connFlags = 0; + if ( $this->mFlags & DBO_SSL ) { + $connFlags |= MYSQLI_CLIENT_SSL; + } + if ( $this->mFlags & DBO_COMPRESS ) { + $connFlags |= MYSQLI_CLIENT_COMPRESS; + } + if ( $this->mFlags & DBO_PERSISTENT ) { + $realServer = 'p:' . $realServer; + } + + $mysqli = mysqli_init(); + $numAttempts = 2; + + for ( $i = 0; $i < $numAttempts; $i++ ) { + if ( $i > 1 ) { + usleep( 1000 ); + } + if ( $mysqli->real_connect( $realServer, $this->mUser, + $this->mPassword, $this->mDBname, null, null, $connFlags ) ) + { + return $mysqli; + } + } + + return false; + } + + /** + * @return bool + */ + protected function closeConnection() { + return $this->mConn->close(); + } + + /** + * @return int + */ + function insertId() { + return $this->mConn->insert_id; + } + + /** + * @return int + */ + function lastErrno() { + if ( $this->mConn ) { + return $this->mConn->errno; + } else { + return mysqli_connect_errno(); + } + } + + /** + * @return int + */ + function affectedRows() { + return $this->mConn->affected_rows; + } + + /** + * @param $db + * @return bool + */ + function selectDB( $db ) { + $this->mDBname = $db; + return $this->mConn->select_db( $db ); + } + + /** + * @return string + */ + function getServerVersion() { + return $this->mConn->server_info; + } + + protected function mysqlFreeResult( $res ) { + $res->free_result(); + return true; + } + + protected function mysqlFetchObject( $res ) { + $object = $res->fetch_object(); + if ( $object === null ) { + return false; + } + return $object; + } + + protected function mysqlFetchArray( $res ) { + $array = $res->fetch_array(); + if ( $array === null ) { + return false; + } + return $array; + } + + protected function mysqlNumRows( $res ) { + return $res->num_rows; + } + + protected function mysqlNumFields( $res ) { + return $res->field_count; + } + + protected function mysqlFetchField( $res, $n ) { + $field = $res->fetch_field_direct( $n ); + $field->not_null = $field->flags & MYSQLI_NOT_NULL_FLAG; + $field->primary_key = $field->flags & MYSQLI_PRI_KEY_FLAG; + $field->unique_key = $field->flags & MYSQLI_UNIQUE_KEY_FLAG; + $field->multiple_key = $field->flags & MYSQLI_MULTIPLE_KEY_FLAG; + $field->binary = $field->flags & MYSQLI_BINARY_FLAG; + return $field; + } + + protected function mysqlFieldName( $res, $n ) { + $field = $res->fetch_field_direct( $n ); + return $field->name; + } + + protected function mysqlDataSeek( $res, $row ) { + return $res->data_seek( $row ); + } + + protected function mysqlError( $conn = null ) { + if ($conn === null) { + return mysqli_connect_error(); + } else { + return $conn->error; + } + } + + protected function mysqlRealEscapeString( $s ) { + return $this->mConn->real_escape_string( $s ); + } + + protected function mysqlPing() { + return $this->mConn->ping(); + } + +} diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 7d8884fb..32d4d984 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -23,7 +23,7 @@ /** * The oci8 extension is fairly weak and doesn't support oci_num_rows, among - * other things. We use a wrapper class to handle that and other + * other things. We use a wrapper class to handle that and other * Oracle-specific bits, like converting column names back to lowercase. * @ingroup Database */ @@ -69,7 +69,7 @@ class ORAResult { $this->nrows = count( $this->rows ); } - if ($this->nrows > 0) { + if ( $this->nrows > 0 ) { foreach ( $this->rows[0] as $k => $v ) { $this->columns[$k] = strtolower( oci_field_name( $stmt, $k + 1 ) ); } @@ -80,7 +80,7 @@ class ORAResult { } public function free() { - unset($this->db); + unset( $this->db ); } public function seek( $row ) { @@ -92,7 +92,7 @@ class ORAResult { } public function numFields() { - return count($this->columns); + return count( $this->columns ); } public function fetchObject() { @@ -206,7 +206,7 @@ class DatabaseOracle extends DatabaseBase { } function __destruct() { - if ($this->mOpened) { + if ( $this->mOpened ) { wfSuppressWarnings(); $this->close(); wfRestoreWarnings(); @@ -241,9 +241,15 @@ class DatabaseOracle extends DatabaseBase { /** * Usually aborts on failure + * @param string $server + * @param string $user + * @param string $password + * @param string $dbName + * @throws DBConnectionError * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { + global $wgDBOracleDRCP; if ( !function_exists( 'oci_connect' ) ) { throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" ); } @@ -271,9 +277,16 @@ class DatabaseOracle extends DatabaseBase { return; } + if ( $wgDBOracleDRCP ) { + $this->setFlag( DBO_PERSISTENT ); + } + $session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT; + wfSuppressWarnings(); - if ( $this->mFlags & DBO_DEFAULT ) { + if ( $this->mFlags & DBO_PERSISTENT ) { + $this->mConn = oci_pconnect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode ); + } elseif ( $this->mFlags & DBO_DEFAULT ) { $this->mConn = oci_new_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode ); } else { $this->mConn = oci_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode ); @@ -313,13 +326,13 @@ class DatabaseOracle extends DatabaseBase { protected function doQuery( $sql ) { wfDebug( "SQL: [$sql]\n" ); - if ( !mb_check_encoding( $sql ) ) { + if ( !StringUtils::isUtf8( $sql ) ) { throw new MWException( "SQL encoding is invalid\n$sql" ); } // handle some oracle specifics // remove AS column/table/subquery namings - if( !$this->getFlag( DBO_DDLMODE ) ) { + if ( !$this->getFlag( DBO_DDLMODE ) ) { $sql = preg_replace( '/ as /i', ' ', $sql ); } @@ -328,7 +341,7 @@ class DatabaseOracle extends DatabaseBase { $union_unique = ( preg_match( '/\/\* UNION_UNIQUE \*\/ /', $sql ) != 0 ); // EXPLAIN syntax in Oracle is EXPLAIN PLAN FOR and it return nothing // you have to select data from plan table after explain - $explain_id = date( 'dmYHis' ); + $explain_id = MWTimestamp::getLocalInstance()->format( 'dmYHis' ); $sql = preg_replace( '/^EXPLAIN /', 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', $sql, 1, $explain_count ); @@ -451,15 +464,15 @@ class DatabaseOracle extends DatabaseBase { * If errors are explicitly ignored, returns NULL on failure * @return bool */ - function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) { + function indexInfo( $table, $index, $fname = __METHOD__ ) { return false; } - function indexUnique( $table, $index, $fname = 'DatabaseOracle::indexUnique' ) { + function indexUnique( $table, $index, $fname = __METHOD__ ) { return false; } - function insert( $table, $a, $fname = 'DatabaseOracle::insert', $options = array() ) { + function insert( $table, $a, $fname = __METHOD__, $options = array() ) { if ( !count( $a ) ) { return true; } @@ -488,7 +501,7 @@ class DatabaseOracle extends DatabaseBase { return $retVal; } - private function fieldBindStatement ( $table, $col, &$val, $includeCol = false ) { + private function fieldBindStatement( $table, $col, &$val, $includeCol = false ) { $col_info = $this->fieldInfoMulti( $table, $col ); $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; @@ -619,7 +632,7 @@ class DatabaseOracle extends DatabaseBase { oci_free_statement( $stmt ); } - function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseOracle::insertSelect', + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); @@ -628,7 +641,7 @@ class DatabaseOracle extends DatabaseBase { } list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); if ( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); + $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); } else { $srcTable = $this->tableName( $srcTable ); } @@ -672,7 +685,7 @@ class DatabaseOracle extends DatabaseBase { Using uppercase because that's the only way Oracle can handle quoted tablenames */ - switch( $name ) { + switch ( $name ) { case 'user': $name = 'MWUSER'; break; @@ -681,12 +694,12 @@ class DatabaseOracle extends DatabaseBase { break; } - return parent::tableName( strtoupper( $name ), $format ); + return strtoupper( parent::tableName( $name, $format ) ); } function tableNameInternal( $name ) { $name = $this->tableName( $name ); - return preg_replace( '/.*\.(.*)/', '$1', $name); + return preg_replace( '/.*\.(.*)/', '$1', $name ); } /** * Return the next in a sequence, save the value for retrieval via insertId() @@ -751,14 +764,14 @@ class DatabaseOracle extends DatabaseBase { function unionQueries( $sqls, $all ) { $glue = ' UNION ALL '; - return 'SELECT * ' . ( $all ? '':'/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')' ; + return 'SELECT * ' . ( $all ? '' : '/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')'; } function wasDeadlock() { return $this->lastErrno() == 'OCI-00060'; } - function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseOracle::duplicateTableStructure' ) { + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) { $temporary = $temporary ? 'TRUE' : 'FALSE'; $newName = strtoupper( $newName ); @@ -771,10 +784,10 @@ class DatabaseOracle extends DatabaseBase { return $this->doQuery( "BEGIN DUPLICATE_TABLE( '$tabName', '$oldPrefix', '$newPrefix', $temporary ); END;" ); } - function listTables( $prefix = null, $fname = 'DatabaseOracle::listTables' ) { + function listTables( $prefix = null, $fname = __METHOD__ ) { $listWhere = ''; - if (!empty($prefix)) { - $listWhere = ' AND table_name LIKE \''.strtoupper($prefix).'%\''; + if ( !empty( $prefix ) ) { + $listWhere = ' AND table_name LIKE \'' . strtoupper( $prefix ) . '%\''; } $owner = strtoupper( $this->mDBname ); @@ -782,21 +795,22 @@ class DatabaseOracle extends DatabaseBase { // dirty code ... i know $endArray = array(); - $endArray[] = strtoupper($prefix.'MWUSER'); - $endArray[] = strtoupper($prefix.'PAGE'); - $endArray[] = strtoupper($prefix.'IMAGE'); + $endArray[] = strtoupper( $prefix . 'MWUSER' ); + $endArray[] = strtoupper( $prefix . 'PAGE' ); + $endArray[] = strtoupper( $prefix . 'IMAGE' ); $fixedOrderTabs = $endArray; - while (($row = $result->fetchRow()) !== false) { - if (!in_array($row['table_name'], $fixedOrderTabs)) + while ( ( $row = $result->fetchRow() ) !== false ) { + if ( !in_array( $row['table_name'], $fixedOrderTabs ) ) { $endArray[] = $row['table_name']; + } } return $endArray; } - public function dropTable( $tableName, $fName = 'DatabaseOracle::dropTable' ) { - $tableName = $this->tableName($tableName); - if( !$this->tableExists( $tableName ) ) { + public function dropTable( $tableName, $fName = __METHOD__ ) { + $tableName = $this->tableName( $tableName ); + if ( !$this->tableExists( $tableName ) ) { return false; } @@ -831,8 +845,8 @@ class DatabaseOracle extends DatabaseBase { /** * @return string wikitext of a link to the server software's web site */ - public static function getSoftwareLink() { - return '[http://www.oracle.com/ Oracle]'; + public function getSoftwareLink() { + return '[{{int:version-db-oracle-url}} Oracle]'; } /** @@ -841,7 +855,7 @@ class DatabaseOracle extends DatabaseBase { function getServerVersion() { //better version number, fallback on driver $rset = $this->doQuery( 'SELECT version FROM product_component_version WHERE UPPER(product) LIKE \'ORACLE DATABASE%\'' ); - if ( !( $row = $rset->fetchRow() ) ) { + if ( !( $row = $rset->fetchRow() ) ) { return oci_server_version( $this->mConn ); } return $row['version']; @@ -851,7 +865,7 @@ class DatabaseOracle extends DatabaseBase { * Query whether a given index exists * @return bool */ - function indexExists( $table, $index, $fname = 'DatabaseOracle::indexExists' ) { + function indexExists( $table, $index, $fname = __METHOD__ ) { $table = $this->tableName( $table ); $table = strtoupper( $this->removeIdentifierQuotes( $table ) ); $index = strtoupper( $index ); @@ -869,7 +883,7 @@ class DatabaseOracle extends DatabaseBase { /** * Query whether a given table exists (in the given schema, or the default mw one if not given) - * @return int + * @return bool */ function tableExists( $table, $fname = __METHOD__ ) { $table = $this->tableName( $table ); @@ -877,13 +891,14 @@ class DatabaseOracle extends DatabaseBase { $owner = $this->addQuotes( strtoupper( $this->mDBname ) ); $SQL = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table"; $res = $this->doQuery( $SQL ); - if ( $res ) { - $count = $res->numRows(); - $res->free(); + if ( $res && $res->numRows() > 0 ) { + $exists = true; } else { - $count = 0; + $exists = false; } - return $count; + + $res->free(); + return $exists; } /** @@ -901,8 +916,8 @@ class DatabaseOracle extends DatabaseBase { if ( is_array( $table ) ) { $table = array_map( array( &$this, 'tableNameInternal' ), $table ); $tableWhere = 'IN ('; - foreach( $table as &$singleTable ) { - $singleTable = $this->removeIdentifierQuotes($singleTable); + foreach ( $table as &$singleTable ) { + $singleTable = $this->removeIdentifierQuotes( $singleTable ); if ( isset( $this->mFieldInfoCache["$singleTable.$field"] ) ) { return $this->mFieldInfoCache["$singleTable.$field"]; } @@ -910,14 +925,14 @@ class DatabaseOracle extends DatabaseBase { } $tableWhere = rtrim( $tableWhere, ',' ) . ')'; } else { - $table = $this->removeIdentifierQuotes( $this->tableNameInternal( $table ) ); + $table = $this->removeIdentifierQuotes( $this->tableNameInternal( $table ) ); if ( isset( $this->mFieldInfoCache["$table.$field"] ) ) { return $this->mFieldInfoCache["$table.$field"]; } - $tableWhere = '= \''.$table.'\''; + $tableWhere = '= \'' . $table . '\''; } - $fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name '.$tableWhere.' and column_name = \''.$field.'\'' ); + $fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name ' . $tableWhere . ' and column_name = \'' . $field . '\'' ); if ( oci_execute( $fieldInfoStmt, $this->execFlags() ) === false ) { $e = oci_error( $fieldInfoStmt ); $this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ ); @@ -926,7 +941,7 @@ class DatabaseOracle extends DatabaseBase { $res = new ORAResult( $this, $fieldInfoStmt ); if ( $res->numRows() == 0 ) { if ( is_array( $table ) ) { - foreach( $table as &$singleTable ) { + foreach ( $table as &$singleTable ) { $this->mFieldInfoCache["$singleTable.$field"] = false; } } else { @@ -952,15 +967,15 @@ class DatabaseOracle extends DatabaseBase { if ( is_array( $table ) ) { throw new DBUnexpectedError( $this, 'DatabaseOracle::fieldInfo called with table array!' ); } - return $this->fieldInfoMulti ($table, $field); + return $this->fieldInfoMulti( $table, $field ); } - protected function doBegin( $fname = 'DatabaseOracle::begin' ) { + protected function doBegin( $fname = __METHOD__ ) { $this->mTrxLevel = 1; $this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' ); } - protected function doCommit( $fname = 'DatabaseOracle::commit' ) { + protected function doCommit( $fname = __METHOD__ ) { if ( $this->mTrxLevel ) { $ret = oci_commit( $this->mConn ); if ( !$ret ) { @@ -971,7 +986,7 @@ class DatabaseOracle extends DatabaseBase { } } - protected function doRollback( $fname = 'DatabaseOracle::rollback' ) { + protected function doRollback( $fname = __METHOD__ ) { if ( $this->mTrxLevel ) { oci_rollback( $this->mConn ); $this->mTrxLevel = 0; @@ -981,7 +996,7 @@ class DatabaseOracle extends DatabaseBase { /* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */ function sourceStream( $fp, $lineCallback = false, $resultCallback = false, - $fname = 'DatabaseOracle::sourceStream', $inputCallback = false ) { + $fname = __METHOD__, $inputCallback = false ) { $cmd = ''; $done = false; $dollarquote = false; @@ -1061,7 +1076,7 @@ class DatabaseOracle extends DatabaseBase { if ( $db == null || $db == $this->mUser ) { return true; } - $sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper($db); + $sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper( $db ); $stmt = oci_parse( $this->mConn, $sql ); wfSuppressWarnings(); $success = oci_execute( $stmt ); @@ -1096,11 +1111,11 @@ class DatabaseOracle extends DatabaseBase { } public function removeIdentifierQuotes( $s ) { - return strpos($s, '/*Q*/') === FALSE ? $s : substr($s, 5); + return strpos( $s, '/*Q*/' ) === false ? $s : substr( $s, 5 ); } public function isQuotedIdentifier( $s ) { - return strpos($s, '/*Q*/') !== FALSE; + return strpos( $s, '/*Q*/' ) !== false; } private function wrapFieldForWhere( $table, &$col, &$val ) { @@ -1111,21 +1126,21 @@ class DatabaseOracle extends DatabaseBase { if ( $col_type == 'CLOB' ) { $col = 'TO_CHAR(' . $col . ')'; $val = $wgContLang->checkTitleEncoding( $val ); - } elseif ( $col_type == 'VARCHAR2' && !mb_check_encoding( $val ) ) { + } elseif ( $col_type == 'VARCHAR2' ) { $val = $wgContLang->checkTitleEncoding( $val ); } } - private function wrapConditionsForWhere ( $table, $conds, $parentCol = null ) { + private function wrapConditionsForWhere( $table, $conds, $parentCol = null ) { $conds2 = array(); foreach ( $conds as $col => $val ) { if ( is_array( $val ) ) { - $conds2[$col] = $this->wrapConditionsForWhere ( $table, $val, $col ); + $conds2[$col] = $this->wrapConditionsForWhere( $table, $val, $col ); } else { if ( is_numeric( $col ) && $parentCol != null ) { - $this->wrapFieldForWhere ( $table, $parentCol, $val ); + $this->wrapFieldForWhere( $table, $parentCol, $val ); } else { - $this->wrapFieldForWhere ( $table, $col, $val ); + $this->wrapFieldForWhere( $table, $col, $val ); } $conds2[$col] = $val; } @@ -1133,8 +1148,8 @@ class DatabaseOracle extends DatabaseBase { return $conds2; } - function selectRow( $table, $vars, $conds, $fname = 'DatabaseOracle::selectRow', $options = array(), $join_conds = array() ) { - if ( is_array($conds) ) { + function selectRow( $table, $vars, $conds, $fname = __METHOD__, $options = array(), $join_conds = array() ) { + if ( is_array( $conds ) ) { $conds = $this->wrapConditionsForWhere( $table, $conds ); } return parent::selectRow( $table, $vars, $conds, $fname, $options, $join_conds ); @@ -1146,7 +1161,7 @@ class DatabaseOracle extends DatabaseBase { * * @private * - * @param $options Array: an associative array of options to be turned into + * @param array $options an associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return array */ @@ -1161,15 +1176,14 @@ class DatabaseOracle extends DatabaseBase { } } - if ( isset( $options['GROUP BY'] ) ) { - $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; - } - if ( isset( $options['ORDER BY'] ) ) { - $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; + $preLimitTail .= $this->makeGroupByWithHaving( $options ); + + $preLimitTail .= $this->makeOrderBy( $options ); + + if ( isset( $noKeyOptions['FOR UPDATE'] ) ) { + $postLimitTail .= ' FOR UPDATE'; } - # if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE'; - # if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE'; if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { $startOpts .= 'DISTINCT'; } @@ -1183,14 +1197,14 @@ class DatabaseOracle extends DatabaseBase { return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } - public function delete( $table, $conds, $fname = 'DatabaseOracle::delete' ) { - if ( is_array($conds) ) { + public function delete( $table, $conds, $fname = __METHOD__ ) { + if ( is_array( $conds ) ) { $conds = $this->wrapConditionsForWhere( $table, $conds ); } // a hack for deleting pages, users and images (which have non-nullable FKs) // all deletions on these tables have transactions so final failure rollbacks these updates $table = $this->tableName( $table ); - if ( $table == $this->tableName( 'user' ) ) { + if ( $table == $this->tableName( 'user' ) ) { $this->update( 'archive', array( 'ar_user' => 0 ), array( 'ar_user' => $conds['user_id'] ), $fname ); $this->update( 'ipblocks', array( 'ipb_user' => 0 ), array( 'ipb_user' => $conds['user_id'] ), $fname ); $this->update( 'image', array( 'img_user' => 0 ), array( 'img_user' => $conds['user_id'] ), $fname ); @@ -1200,13 +1214,13 @@ class DatabaseOracle extends DatabaseBase { $this->update( 'uploadstash', array( 'us_user' => 0 ), array( 'us_user' => $conds['user_id'] ), $fname ); $this->update( 'recentchanges', array( 'rc_user' => 0 ), array( 'rc_user' => $conds['user_id'] ), $fname ); $this->update( 'logging', array( 'log_user' => 0 ), array( 'log_user' => $conds['user_id'] ), $fname ); - } elseif ( $table == $this->tableName( 'image' ) ) { + } elseif ( $table == $this->tableName( 'image' ) ) { $this->update( 'oldimage', array( 'oi_name' => 0 ), array( 'oi_name' => $conds['img_name'] ), $fname ); } return parent::delete( $table, $conds, $fname ); } - function update( $table, $values, $conds, $fname = 'DatabaseOracle::update', $options = array() ) { + function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { global $wgContLang; $table = $this->tableName( $table ); diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 457bf384..aed35f10 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -117,7 +117,7 @@ SQL; * @since 1.19 */ function defaultValue() { - if( $this->has_default ) { + if ( $this->has_default ) { return $this->default; } else { return false; @@ -139,15 +139,15 @@ class PostgresTransactionState { array( "desc" => "%s: Connection state changed from %s -> %s\n", "states" => array( - PGSQL_CONNECTION_OK => "OK", - PGSQL_CONNECTION_BAD => "BAD" + PGSQL_CONNECTION_OK => "OK", + PGSQL_CONNECTION_BAD => "BAD" ) ), array( "desc" => "%s: Transaction state changed from %s -> %s\n", "states" => array( - PGSQL_TRANSACTION_IDLE => "IDLE", - PGSQL_TRANSACTION_ACTIVE => "ACTIVE", + PGSQL_TRANSACTION_IDLE => "IDLE", + PGSQL_TRANSACTION_ACTIVE => "ACTIVE", PGSQL_TRANSACTION_INTRANS => "TRANS", PGSQL_TRANSACTION_INERROR => "ERROR", PGSQL_TRANSACTION_UNKNOWN => "UNKNOWN" @@ -176,8 +176,8 @@ class PostgresTransactionState { $old = reset( $this->mCurrentState ); $new = reset( $this->mNewState ); foreach ( self::$WATCHED as $watched ) { - if ($old !== $new) { - $this->log_changed($old, $new, $watched); + if ( $old !== $new ) { + $this->log_changed( $old, $new, $watched ); } $old = next( $this->mCurrentState ); $new = next( $this->mNewState ); @@ -189,7 +189,7 @@ class PostgresTransactionState { } protected function describe_changed( $status, $desc_table ) { - if( isset( $desc_table[$status] ) ) { + if ( isset( $desc_table[$status] ) ) { return $desc_table[$status]; } else { return "STATUS " . $status; @@ -197,11 +197,11 @@ class PostgresTransactionState { } protected function log_changed( $old, $new, $watched ) { - wfDebug(sprintf($watched["desc"], + wfDebug( sprintf( $watched["desc"], $this->mConn, $this->describe_changed( $old, $watched["states"] ), - $this->describe_changed( $new, $watched["states"] )) - ); + $this->describe_changed( $new, $watched["states"] ) + ) ); } } @@ -218,7 +218,7 @@ class SavepointPostgres { protected $id; protected $didbegin; - public function __construct ($dbw, $id) { + public function __construct( $dbw, $id ) { $this->dbw = $dbw; $this->id = $id; $this->didbegin = false; @@ -232,12 +232,14 @@ class SavepointPostgres { public function __destruct() { if ( $this->didbegin ) { $this->dbw->rollback(); + $this->didbegin = false; } } public function commit() { if ( $this->didbegin ) { $this->dbw->commit(); + $this->didbegin = false; } } @@ -245,29 +247,29 @@ class SavepointPostgres { global $wgDebugDBTransactions; if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) { if ( $wgDebugDBTransactions ) { - wfDebug( sprintf ($msg_ok, $this->id ) ); + wfDebug( sprintf ( $msg_ok, $this->id ) ); } } else { - wfDebug( sprintf ($msg_failed, $this->id ) ); + wfDebug( sprintf ( $msg_failed, $this->id ) ); } } public function savepoint() { - $this->query("SAVEPOINT", + $this->query( "SAVEPOINT", "Transaction state: savepoint \"%s\" established.\n", "Transaction state: establishment of savepoint \"%s\" FAILED.\n" ); } public function release() { - $this->query("RELEASE", + $this->query( "RELEASE", "Transaction state: savepoint \"%s\" released.\n", "Transaction state: release of savepoint \"%s\" FAILED.\n" ); } public function rollback() { - $this->query("ROLLBACK TO", + $this->query( "ROLLBACK TO", "Transaction state: savepoint \"%s\" rolled back.\n", "Transaction state: rollback of savepoint \"%s\" FAILED.\n" ); @@ -318,13 +320,18 @@ class DatabasePostgres extends DatabaseBase { function hasConstraint( $name ) { $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . - pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) ."'"; + pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'"; $res = $this->doQuery( $SQL ); return $this->numRows( $res ); } /** * Usually aborts on failure + * @param string $server + * @param string $user + * @param string $password + * @param string $dbName + * @throws DBConnectionError * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { @@ -386,6 +393,9 @@ class DatabasePostgres extends DatabaseBase { $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ ); $this->query( "SET timezone = 'GMT'", __METHOD__ ); $this->query( "SET standard_conforming_strings = on", __METHOD__ ); + if ( $this->getServerVersion() >= 9.0 ) { + $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127 + } global $wgDBmwschema; $this->determineCoreSchema( $wgDBmwschema ); @@ -428,7 +438,7 @@ class DatabasePostgres extends DatabaseBase { $sql = mb_convert_encoding( $sql, 'UTF-8' ); } $this->mTransactionState->check(); - if( pg_send_query( $this->mConn, $sql ) === false ) { + if ( pg_send_query( $this->mConn, $sql ) === false ) { throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" ); } $this->mLastResult = pg_get_result( $this->mConn ); @@ -440,7 +450,7 @@ class DatabasePostgres extends DatabaseBase { return $this->mLastResult; } - protected function dumpError () { + protected function dumpError() { $diags = array( PGSQL_DIAG_SEVERITY, PGSQL_DIAG_SQLSTATE, PGSQL_DIAG_MESSAGE_PRIMARY, @@ -454,7 +464,7 @@ class DatabasePostgres extends DatabaseBase { PGSQL_DIAG_SOURCE_LINE, PGSQL_DIAG_SOURCE_FUNCTION ); foreach ( $diags as $d ) { - wfDebug( sprintf("PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) ); + wfDebug( sprintf( "PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) ); } } @@ -472,8 +482,7 @@ class DatabasePostgres extends DatabaseBase { parent::reportQueryError( $error, $errno, $sql, $fname, false ); } - - function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) { + function queryIgnore( $sql, $fname = __METHOD__ ) { return $this->query( $sql, $fname, true ); } @@ -500,7 +509,7 @@ class DatabasePostgres extends DatabaseBase { # @todo hashar: not sure if the following test really trigger if the object # fetching failed. - if( pg_last_error( $this->mConn ) ) { + if ( pg_last_error( $this->mConn ) ) { throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); } return $row; @@ -513,7 +522,7 @@ class DatabasePostgres extends DatabaseBase { wfSuppressWarnings(); $row = pg_fetch_array( $res ); wfRestoreWarnings(); - if( pg_last_error( $this->mConn ) ) { + if ( pg_last_error( $this->mConn ) ) { throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); } return $row; @@ -526,7 +535,7 @@ class DatabasePostgres extends DatabaseBase { wfSuppressWarnings(); $n = pg_num_rows( $res ); wfRestoreWarnings(); - if( pg_last_error( $this->mConn ) ) { + if ( pg_last_error( $this->mConn ) ) { throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); } return $n; @@ -547,8 +556,10 @@ class DatabasePostgres extends DatabaseBase { } /** - * This must be called after nextSequenceVal - * @return null + * Return the result of the last call to nextSequenceValue(); + * This must be called after nextSequenceValue(). + * + * @return integer|null */ function insertId() { return $this->mInsertId; @@ -585,7 +596,7 @@ class DatabasePostgres extends DatabaseBase { // Forced result for simulated queries return $this->mAffectedRows; } - if( empty( $this->mLastResult ) ) { + if ( empty( $this->mLastResult ) ) { return 0; } return pg_affected_rows( $this->mLastResult ); @@ -599,14 +610,14 @@ class DatabasePostgres extends DatabaseBase { * Takes same arguments as Database::select() * @return int */ - function estimateRowCount( $table, $vars = '*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) { + function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) { $options['EXPLAIN'] = true; $res = $this->select( $table, $vars, $conds, $fname, $options ); $rows = -1; if ( $res ) { $row = $this->fetchRow( $res ); $count = array(); - if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) { + if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) { $rows = $count[1]; } } @@ -618,7 +629,7 @@ class DatabasePostgres extends DatabaseBase { * If errors are explicitly ignored, returns NULL on failure * @return bool|null */ - function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) { + function indexInfo( $table, $index, $fname = __METHOD__ ) { $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'"; $res = $this->query( $sql, $fname ); if ( !$res ) { @@ -638,9 +649,10 @@ class DatabasePostgres extends DatabaseBase { * @since 1.19 * @return Array */ - function indexAttributes ( $index, $schema = false ) { - if ( $schema === false ) + function indexAttributes( $index, $schema = false ) { + if ( $schema === false ) { $schema = $this->getCoreSchema(); + } /* * A subquery would be not needed if we didn't care about the order * of attributes, but we do @@ -677,7 +689,7 @@ class DatabasePostgres extends DatabaseBase { AND i.indclass[s.g] = opcls.oid AND pg_am.oid = opcls.opcmethod __INDEXATTR__; - $res = $this->query($sql, __METHOD__); + $res = $this->query( $sql, __METHOD__ ); $a = array(); if ( $res ) { foreach ( $res as $row ) { @@ -685,7 +697,7 @@ __INDEXATTR__; $row->attname, $row->opcname, $row->amname, - $row->option); + $row->option ); } } else { return null; @@ -693,9 +705,8 @@ __INDEXATTR__; return $a; } - - function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) { - $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'". + function indexUnique( $table, $index, $fname = __METHOD__ ) { + $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" . " AND indexdef LIKE 'CREATE UNIQUE%(" . $this->strencode( $this->indexName( $index ) ) . ")'"; @@ -710,6 +721,29 @@ __INDEXATTR__; } /** + * Change the FOR UPDATE option as necessary based on the join conditions. Then pass + * to the parent function to get the actual SQL text. + * + * In Postgres when using FOR UPDATE, only the main table and tables that are inner joined + * can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do + * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly. + */ + function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) { + $forUpdateKey = array_search( 'FOR UPDATE', $options ); + if ( $forUpdateKey !== false && $join_conds ) { + unset( $options[$forUpdateKey] ); + + foreach ( $join_conds as $table => $join_cond ) { + if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) { + $options['FOR UPDATE'][] = $table; + } + } + } + + return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); + } + + /** * INSERT wrapper, inserts an array into a table * * $args may be a single associative array, or an array of these with numeric keys, @@ -718,17 +752,17 @@ __INDEXATTR__; * @param $table String: Name of the table to insert to. * @param $args Array: Items to insert into the table. * @param $fname String: Name of the function, for profiling - * @param $options String or Array. Valid options: IGNORE + * @param string $options or Array. Valid options: IGNORE * * @return bool Success of insert operation. IGNORE always returns true. */ - function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) { + function insert( $table, $args, $fname = __METHOD__, $options = array() ) { if ( !count( $args ) ) { return true; } $table = $this->tableName( $table ); - if (! isset( $this->numeric_version ) ) { + if ( !isset( $this->numeric_version ) ) { $this->getServerVersion(); } @@ -839,12 +873,12 @@ __INDEXATTR__; * @todo FIXME: Implement this a little better (seperate select/insert)? * @return bool */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect', + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); - if( !is_array( $insertOptions ) ) { + if ( !is_array( $insertOptions ) ) { $insertOptions = array( $insertOptions ); } @@ -860,11 +894,11 @@ __INDEXATTR__; $savepoint->savepoint(); } - if( !is_array( $selectOptions ) ) { + if ( !is_array( $selectOptions ) ) { $selectOptions = array( $selectOptions ); } list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); - if( is_array( $srcTable ) ) { + if ( is_array( $srcTable ) ) { $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); } else { $srcTable = $this->tableName( $srcTable ); @@ -881,9 +915,9 @@ __INDEXATTR__; $sql .= " $tailOpts"; $res = (bool)$this->query( $sql, $fname, $savepoint ); - if( $savepoint ) { + if ( $savepoint ) { $bar = pg_last_error(); - if( $bar != false ) { + if ( $bar != false ) { $savepoint->rollback(); } else { $savepoint->release(); @@ -904,7 +938,7 @@ __INDEXATTR__; function tableName( $name, $format = 'quoted' ) { # Replace reserved words with better ones - switch( $name ) { + switch ( $name ) { case 'user': return $this->realTableName( 'mwuser', $format ); case 'text': @@ -950,7 +984,7 @@ __INDEXATTR__; FROM pg_class c, pg_attribute a, pg_type t WHERE relname='$table' AND a.attrelid=c.oid AND a.atttypid=t.oid and a.attname='$field'"; - $res =$this->query( $sql ); + $res = $this->query( $sql ); $row = $this->fetchObject( $res ); if ( $row->ftype == 'varchar' ) { $size = $row->size - 4; @@ -968,21 +1002,21 @@ __INDEXATTR__; return $this->lastErrno() == '40P01'; } - function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) { + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) { $newName = $this->addIdentifierQuotes( $newName ); $oldName = $this->addIdentifierQuotes( $oldName ); return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname ); } - function listTables( $prefix = null, $fname = 'DatabasePostgres::listTables' ) { + function listTables( $prefix = null, $fname = __METHOD__ ) { $eschema = $this->addQuotes( $this->getCoreSchema() ); $result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname ); $endArray = array(); - foreach( $result as $table ) { - $vars = get_object_vars($table); + foreach ( $result as $table ) { + $vars = get_object_vars( $table ); $table = array_pop( $vars ); - if( !$prefix || strpos( $table, $prefix ) === 0 ) { + if ( !$prefix || strpos( $table, $prefix ) === 0 ) { $endArray[] = $table; } } @@ -1013,26 +1047,26 @@ __INDEXATTR__; * @return string */ function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) { - if( false === $limit ) { - $limit = strlen( $text )-1; + if ( false === $limit ) { + $limit = strlen( $text ) - 1; $output = array(); } - if( '{}' == $text ) { + if ( '{}' == $text ) { return $output; } do { - if ( '{' != $text{$offset} ) { + if ( '{' != $text[$offset] ) { preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/", $text, $match, 0, $offset ); $offset += strlen( $match[0] ); - $output[] = ( '"' != $match[1]{0} + $output[] = ( '"' != $match[1][0] ? $match[1] : stripcslashes( substr( $match[1], 1, -1 ) ) ); if ( '},' == $match[3] ) { return $output; } } else { - $offset = $this->pg_array_parse( $text, $output, $limit, $offset+1 ); + $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 ); } } while ( $limit > $offset ); return $output; @@ -1048,11 +1082,10 @@ __INDEXATTR__; /** * @return string wikitext of a link to the server software's web site */ - public static function getSoftwareLink() { - return '[http://www.postgresql.org/ PostgreSQL]'; + public function getSoftwareLink() { + return '[{{int:version-db-postgres-url}} PostgreSQL]'; } - /** * Return current schema (executes SELECT current_schema()) * Needs transaction @@ -1061,7 +1094,7 @@ __INDEXATTR__; * @return string return default schema for the current session */ function getCurrentSchema() { - $res = $this->query( "SELECT current_schema()", __METHOD__); + $res = $this->query( "SELECT current_schema()", __METHOD__ ); $row = $this->fetchRow( $res ); return $row[0]; } @@ -1071,17 +1104,17 @@ __INDEXATTR__; * This is list does not contain magic keywords like "$user" * Needs transaction * - * @seealso getSearchPath() - * @seealso setSearchPath() + * @see getSearchPath() + * @see setSearchPath() * @since 1.19 * @return array list of actual schemas for the current sesson */ function getSchemas() { - $res = $this->query( "SELECT current_schemas(false)", __METHOD__); + $res = $this->query( "SELECT current_schemas(false)", __METHOD__ ); $row = $this->fetchRow( $res ); $schemas = array(); /* PHP pgsql support does not support array type, "{a,b}" string is returned */ - return $this->pg_array_parse($row[0], $schemas); + return $this->pg_array_parse( $row[0], $schemas ); } /** @@ -1094,10 +1127,10 @@ __INDEXATTR__; * @return array how to search for table names schemas for the current user */ function getSearchPath() { - $res = $this->query( "SHOW search_path", __METHOD__); + $res = $this->query( "SHOW search_path", __METHOD__ ); $row = $this->fetchRow( $res ); /* PostgreSQL returns SHOW values as strings */ - return explode(",", $row[0]); + return explode( ",", $row[0] ); } /** @@ -1108,7 +1141,7 @@ __INDEXATTR__; * @param $search_path array list of schemas to be searched by default */ function setSearchPath( $search_path ) { - $this->query( "SET search_path = " . implode(", ", $search_path) ); + $this->query( "SET search_path = " . implode( ", ", $search_path ) ); } /** @@ -1129,7 +1162,7 @@ __INDEXATTR__; if ( $this->schemaExists( $desired_schema ) ) { if ( in_array( $desired_schema, $this->getSchemas() ) ) { $this->mCoreSchema = $desired_schema; - wfDebug("Schema \"" . $desired_schema . "\" already in the search path\n"); + wfDebug( "Schema \"" . $desired_schema . "\" already in the search path\n" ); } else { /** * Prepend our schema (e.g. 'mediawiki') in front @@ -1141,11 +1174,11 @@ __INDEXATTR__; $this->addIdentifierQuotes( $desired_schema )); $this->setSearchPath( $search_path ); $this->mCoreSchema = $desired_schema; - wfDebug("Schema \"" . $desired_schema . "\" added to the search path\n"); + wfDebug( "Schema \"" . $desired_schema . "\" added to the search path\n" ); } } else { $this->mCoreSchema = $this->getCurrentSchema(); - wfDebug("Schema \"" . $desired_schema . "\" not found, using current \"". $this->mCoreSchema ."\"\n"); + wfDebug( "Schema \"" . $desired_schema . "\" not found, using current \"" . $this->mCoreSchema . "\"\n" ); } /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */ $this->commit( __METHOD__ ); @@ -1251,8 +1284,8 @@ SQL; } function constraintExists( $table, $constraint ) { - $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints ". - "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", + $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints " . + "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", $this->addQuotes( $this->getCoreSchema() ), $this->addQuotes( $table ), $this->addQuotes( $constraint ) @@ -1340,7 +1373,7 @@ SQL; * * @private * - * @param $ins String: SQL string, read from a stream (usually tables.sql) + * @param string $ins SQL string, read from a stream (usually tables.sql) * * @return string SQL string */ @@ -1364,7 +1397,7 @@ SQL; * * @private * - * @param $options Array: an associative array of options to be turned into + * @param array $options an associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return array */ @@ -1379,23 +1412,9 @@ SQL; } } - if ( isset( $options['GROUP BY'] ) ) { - $gb = is_array( $options['GROUP BY'] ) - ? implode( ',', $options['GROUP BY'] ) - : $options['GROUP BY']; - $preLimitTail .= " GROUP BY {$gb}"; - } - - if ( isset( $options['HAVING'] ) ) { - $preLimitTail .= " HAVING {$options['HAVING']}"; - } + $preLimitTail .= $this->makeGroupByWithHaving( $options ); - if ( isset( $options['ORDER BY'] ) ) { - $ob = is_array( $options['ORDER BY'] ) - ? implode( ',', $options['ORDER BY'] ) - : $options['ORDER BY']; - $preLimitTail .= " ORDER BY {$ob}"; - } + $preLimitTail .= $this->makeOrderBy( $options ); //if ( isset( $options['LIMIT'] ) ) { // $tailOpts .= $this->limitResult( '', $options['LIMIT'], @@ -1403,9 +1422,12 @@ SQL; // : false ); //} - if ( isset( $noKeyOptions['FOR UPDATE'] ) ) { + if ( isset( $options['FOR UPDATE'] ) ) { + $postLimitTail .= ' FOR UPDATE OF ' . implode( ', ', $options['FOR UPDATE'] ); + } else if ( isset( $noKeyOptions['FOR UPDATE'] ) ) { $postLimitTail .= ' FOR UPDATE'; } + if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { $startOpts .= 'DISTINCT'; } @@ -1413,7 +1435,8 @@ SQL; return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } - function setFakeMaster( $enabled = true ) {} + function setFakeMaster( $enabled = true ) { + } function getDBname() { return $this->mDBname; @@ -1443,4 +1466,65 @@ SQL; } return parent::streamStatementEnd( $sql, $newLine ); } + + /** + * Check to see if a named lock is available. This is non-blocking. + * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS + * + * @param string $lockName name of lock to poll + * @param string $method name of method calling us + * @return Boolean + * @since 1.20 + */ + public function lockIsFree( $lockName, $method ) { + $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) ); + $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key)) + WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method ); + $row = $this->fetchObject( $result ); + return ( $row->lockstatus === 't' ); + } + + /** + * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS + * @param $lockName string + * @param $method string + * @param $timeout int + * @return bool + */ + public function lock( $lockName, $method, $timeout = 5 ) { + $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) ); + for ( $attempts = 1; $attempts <= $timeout; ++$attempts ) { + $result = $this->query( + "SELECT pg_try_advisory_lock($key) AS lockstatus", $method ); + $row = $this->fetchObject( $result ); + if ( $row->lockstatus === 't' ) { + return true; + } else { + sleep( 1 ); + } + } + wfDebug( __METHOD__ . " failed to acquire lock\n" ); + return false; + } + + /** + * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS + * @param $lockName string + * @param $method string + * @return bool + */ + public function unlock( $lockName, $method ) { + $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) ); + $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method ); + $row = $this->fetchObject( $result ); + return ( $row->lockstatus === 't' ); + } + + /** + * @param string $lockName + * @return string Integer + */ + private function bigintFromLockName( $lockName ) { + return wfBaseConvert( substr( sha1( $lockName ), 0, 15 ), 16, 10 ); + } } // end DatabasePostgres class diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index f1e553d7..3e034649 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -52,9 +52,9 @@ class DatabaseSqlite extends DatabaseBase { $this->mName = $dbName; parent::__construct( $server, $user, $password, $dbName, $flags ); // parent doesn't open when $user is false, but we can work with $dbName - if( $dbName ) { + if ( $dbName && !$this->isOpen() ) { global $wgSharedDB; - if( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) { + if ( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) { $this->attachDatabase( $wgSharedDB ); } } @@ -68,7 +68,7 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @todo: check if it should be true like parent class + * @todo Check if it should be true like parent class * * @return bool */ @@ -79,16 +79,18 @@ class DatabaseSqlite extends DatabaseBase { /** Open an SQLite database and return a resource handle to it * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases * - * @param $server - * @param $user - * @param $pass - * @param $dbName + * @param string $server + * @param string $user + * @param string $pass + * @param string $dbName * + * @throws DBConnectionError * @return PDO */ function open( $server, $user, $pass, $dbName ) { global $wgSQLiteDataDir; + $this->close(); $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName ); if ( !is_readable( $fileName ) ) { $this->mConn = false; @@ -103,6 +105,7 @@ class DatabaseSqlite extends DatabaseBase { * * @param $fileName string * + * @throws DBConnectionError * @return PDO|bool SQL connection or false if failed */ function openFile( $fileName ) { @@ -125,6 +128,8 @@ class DatabaseSqlite extends DatabaseBase { # set error codes only, don't raise exceptions if ( $this->mOpened ) { $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); + # Enforce LIKE to be case sensitive, just like MySQL + $this->query( 'PRAGMA case_sensitive_like = 1' ); return true; } } @@ -140,8 +145,8 @@ class DatabaseSqlite extends DatabaseBase { /** * Generates a database file name. Explicitly public for installer. - * @param $dir String: Directory where database resides - * @param $dbName String: Database name + * @param string $dir Directory where database resides + * @param string $dbName Database name * @return String */ public static function generateFileName( $dir, $dbName ) { @@ -159,7 +164,7 @@ class DatabaseSqlite extends DatabaseBase { $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ ); if ( $res ) { $row = $res->fetchRow(); - self::$fulltextEnabled = stristr($row['sql'], 'fts' ) !== false; + self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false; } } return self::$fulltextEnabled; @@ -190,13 +195,13 @@ class DatabaseSqlite extends DatabaseBase { * Attaches external database to our connection, see http://sqlite.org/lang_attach.html * for details. * - * @param $name String: database name to be used in queries like SELECT foo FROM dbname.table - * @param $file String: database file name. If omitted, will be generated using $name and $wgSQLiteDataDir - * @param $fname String: calling function name + * @param string $name database name to be used in queries like SELECT foo FROM dbname.table + * @param string $file database file name. If omitted, will be generated using $name and $wgSQLiteDataDir + * @param string $fname calling function name * * @return ResultWrapper */ - function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) { + function attachDatabase( $name, $file = false, $fname = __METHOD__ ) { global $wgSQLiteDataDir; if ( !$file ) { $file = self::generateFileName( $wgSQLiteDataDir, $name ); @@ -248,7 +253,7 @@ class DatabaseSqlite extends DatabaseBase { /** * @param $res ResultWrapper - * @return + * @return object|bool */ function fetchObject( $res ) { if ( $res instanceof ResultWrapper ) { @@ -274,7 +279,7 @@ class DatabaseSqlite extends DatabaseBase { /** * @param $res ResultWrapper - * @return bool|mixed + * @return array|bool */ function fetchRow( $res ) { if ( $res instanceof ResultWrapper ) { @@ -416,7 +421,7 @@ class DatabaseSqlite extends DatabaseBase { * * @return array */ - function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) { + function indexInfo( $table, $index, $fname = __METHOD__ ) { $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')'; $res = $this->query( $sql, $fname ); if ( !$res ) { @@ -438,7 +443,7 @@ class DatabaseSqlite extends DatabaseBase { * @param $fname string * @return bool|null */ - function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) { + function indexUnique( $table, $index, $fname = __METHOD__ ) { $row = $this->selectRow( 'sqlite_master', '*', array( 'type' => 'index', @@ -467,7 +472,7 @@ class DatabaseSqlite extends DatabaseBase { */ function makeSelectOptions( $options ) { foreach ( $options as $k => $v ) { - if ( is_numeric( $k ) && $v == 'FOR UPDATE' ) { + if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) { $options[$k] = ''; } } @@ -510,7 +515,7 @@ class DatabaseSqlite extends DatabaseBase { * Based on generic method (parent) with some prior SQLite-sepcific adjustments * @return bool */ - function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) { + function insert( $table, $a, $fname = __METHOD__, $options = array() ) { if ( !count( $a ) ) { return true; } @@ -537,8 +542,10 @@ class DatabaseSqlite extends DatabaseBase { * @param $fname string * @return bool|ResultWrapper */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) { - if ( !count( $rows ) ) return true; + function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { + if ( !count( $rows ) ) { + return true; + } # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries if ( isset( $rows[0] ) && is_array( $rows[0] ) ) { @@ -593,7 +600,7 @@ class DatabaseSqlite extends DatabaseBase { * @return bool */ function wasErrorReissuable() { - return $this->lastErrno() == 17; // SQLITE_SCHEMA; + return $this->lastErrno() == 17; // SQLITE_SCHEMA; } /** @@ -606,8 +613,8 @@ class DatabaseSqlite extends DatabaseBase { /** * @return string wikitext of a link to the server software's web site */ - public static function getSoftwareLink() { - return "[http://sqlite.org/ SQLite]"; + public function getSoftwareLink() { + return "[{{int:version-db-sqlite-url}} SQLite]"; } /** @@ -649,7 +656,11 @@ class DatabaseSqlite extends DatabaseBase { if ( $this->mTrxLevel == 1 ) { $this->commit( __METHOD__ ); } - $this->mConn->beginTransaction(); + try { + $this->mConn->beginTransaction(); + } catch ( PDOException $e ) { + throw new DBUnexpectedError( $this, 'Error in BEGIN query: ' . $e->getMessage() ); + } $this->mTrxLevel = 1; } @@ -657,7 +668,11 @@ class DatabaseSqlite extends DatabaseBase { if ( $this->mTrxLevel == 0 ) { return; } - $this->mConn->commit(); + try { + $this->mConn->commit(); + } catch ( PDOException $e ) { + throw new DBUnexpectedError( $this, 'Error in COMMIT query: ' . $e->getMessage() ); + } $this->mTrxLevel = 0; } @@ -703,6 +718,16 @@ class DatabaseSqlite extends DatabaseBase { function addQuotes( $s ) { if ( $s instanceof Blob ) { return "x'" . bin2hex( $s->fetch() ) . "'"; + } elseif ( is_bool( $s ) ) { + return (int)$s; + } elseif ( strpos( $s, "\0" ) !== false ) { + // SQLite doesn't support \0 in strings, so use the hex representation as a workaround. + // This is a known limitation of SQLite's mprintf function which PDO should work around, + // but doesn't. I have reported this to php.net as bug #63419: + // https://bugs.php.net/bug.php?id=63419 + // There was already a similar report for SQLite3::escapeString, bug #62361: + // https://bugs.php.net/bug.php?id=62361 + return "x'" . bin2hex( $s ) . "'"; } else { return $this->mConn->quote( $s ); } @@ -801,7 +826,7 @@ class DatabaseSqlite extends DatabaseBase { * @param $fname string * @return bool|ResultWrapper */ - function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) { + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) { $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . $this->addQuotes( $oldName ) . " AND type='table'", $fname ); $obj = $this->fetchObject( $res ); if ( !$obj ) { @@ -819,16 +844,15 @@ class DatabaseSqlite extends DatabaseBase { return $this->query( $sql, $fname ); } - /** * List all tables on the database * - * @param $prefix string Only show tables with this prefix, e.g. mw_ - * @param $fname String: calling function name + * @param string $prefix Only show tables with this prefix, e.g. mw_ + * @param string $fname calling function name * * @return array */ - function listTables( $prefix = null, $fname = 'DatabaseSqlite::listTables' ) { + function listTables( $prefix = null, $fname = __METHOD__ ) { $result = $this->select( 'sqlite_master', 'name', @@ -837,11 +861,11 @@ class DatabaseSqlite extends DatabaseBase { $endArray = array(); - foreach( $result as $table ) { - $vars = get_object_vars($table); + foreach ( $result as $table ) { + $vars = get_object_vars( $table ); $table = array_pop( $vars ); - if( !$prefix || strpos( $table, $prefix ) === 0 ) { + if ( !$prefix || strpos( $table, $prefix ) === 0 ) { if ( strpos( $table, 'sqlite_' ) !== 0 ) { $endArray[] = $table; } diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php index c846788d..de58bab6 100644 --- a/includes/db/DatabaseUtility.php +++ b/includes/db/DatabaseUtility.php @@ -1,6 +1,6 @@ <?php /** - * This file contains database-related utiliy classes. + * This file contains database-related utility classes. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -138,7 +138,7 @@ class ResultWrapper implements Iterator { /** * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. + * form. Fields are retrieved with $row['fieldname']. * * @return Array * @throws DBUnexpectedError Thrown if the database returns an error @@ -219,9 +219,9 @@ class ResultWrapper implements Iterator { * doesn't go anywhere near an actual database. */ class FakeResultWrapper extends ResultWrapper { - var $result = array(); - var $db = null; // And it's going to stay that way :D - var $pos = 0; + var $result = array(); + var $db = null; // And it's going to stay that way :D + var $pos = 0; var $currentRow = null; function __construct( $array ) { @@ -253,7 +253,8 @@ class FakeResultWrapper extends ResultWrapper { $this->pos = $row; } - function free() {} + function free() { + } // Callers want to be able to access fields with $this->fieldName function fetchObject() { @@ -285,7 +286,7 @@ class LikeMatch { /** * Store a string into a LikeMatch marker object. * - * @param String $s + * @param string $s */ public function __construct( $s ) { $this->str = $s; @@ -306,4 +307,3 @@ class LikeMatch { */ interface DBMasterPos { } - diff --git a/includes/db/IORMRow.php b/includes/db/IORMRow.php index e99ba6cc..39411791 100644 --- a/includes/db/IORMRow.php +++ b/includes/db/IORMRow.php @@ -27,28 +27,17 @@ * @file * @ingroup ORM * - * @licence GNU GPL v2 or later + * @license GNU GPL v2 or later * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ interface IORMRow { - - /** - * Constructor. - * - * @since 1.20 - * - * @param IORMTable $table - * @param array|null $fields - * @param boolean $loadDefaults - */ - public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ); - /** * Load the specified fields from the database. * * @since 1.20 + * @deprecated since 1.22 * * @param array|null $fields * @param boolean $override @@ -75,8 +64,9 @@ interface IORMRow { * Gets the value of a field but first loads it if not done so already. * * @since 1.20 + * @deprecated since 1.22 * - * @param string$name + * @param string $name * * @return mixed */ @@ -156,6 +146,7 @@ interface IORMRow { * Load the default values, via getDefaults. * * @since 1.20 + * @deprecated since 1.22 * * @param boolean $override */ @@ -168,6 +159,7 @@ interface IORMRow { * @since 1.20 * * @param string|null $functionName + * @deprecated since 1.22 * * @return boolean Success indicator */ @@ -177,6 +169,7 @@ interface IORMRow { * Removes the object from the database. * * @since 1.20 + * @deprecated since 1.22 * * @return boolean Success indicator */ @@ -216,9 +209,9 @@ interface IORMRow { /** * Add an amount (can be negative) to the specified field (needs to be numeric). - * TODO: most off this stuff makes more sense in the table class * * @since 1.20 + * @deprecated since 1.22 * * @param string $field * @param integer $amount @@ -240,6 +233,7 @@ interface IORMRow { * Computes and updates the values of the summary fields. * * @since 1.20 + * @deprecated since 1.22 * * @param array|string|null $summaryFields */ @@ -249,6 +243,7 @@ interface IORMRow { * Sets the value for the @see $updateSummaries field. * * @since 1.20 + * @deprecated since 1.22 * * @param boolean $update */ @@ -258,6 +253,7 @@ interface IORMRow { * Sets the value for the @see $inSummaryMode field. * * @since 1.20 + * @deprecated since 1.22 * * @param boolean $summaryMode */ @@ -267,9 +263,10 @@ interface IORMRow { * Returns the table this IORMRow is a row in. * * @since 1.20 + * @deprecated since 1.22 * * @return IORMTable */ public function getTable(); -}
\ No newline at end of file +} diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php index 99413f99..36865655 100644 --- a/includes/db/IORMTable.php +++ b/includes/db/IORMTable.php @@ -23,7 +23,7 @@ * @file * @ingroup ORM * - * @licence GNU GPL v2 or later + * @license GNU GPL v2 or later * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ @@ -97,6 +97,8 @@ interface IORMTable { * Selects the the specified fields of the records matching the provided * conditions and returns them as DBDataObject. Field names get prefixed. * + * @see DatabaseBase::select() + * * @since 1.20 * * @param array|string|null $fields @@ -104,7 +106,8 @@ interface IORMTable { * @param array $options * @param string|null $functionName * - * @return ORMResult + * @return ORMResult The result set + * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode) */ public function select( $fields = null, array $conditions = array(), array $options = array(), $functionName = null ); @@ -136,6 +139,7 @@ interface IORMTable { * @param null|string $functionName * * @return ResultWrapper + * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode) */ public function rawSelect( $fields = null, array $conditions = array(), array $options = array(), $functionName = null ); @@ -230,6 +234,15 @@ interface IORMTable { public function has( array $conditions = array() ); /** + * Checks if the table exists + * + * @since 1.21 + * + * @return boolean + */ + public function exists(); + + /** * Returns the amount of matching records. * Condition field names get prefixed. * @@ -299,6 +312,71 @@ interface IORMTable { public function setReadDb( $db ); /** + * Get the ID of the any foreign wiki to use as a target for database operations + * + * @since 1.20 + * + * @return String|bool The target wiki, in a form that LBFactory understands (or false if the local wiki is used) + */ + public function getTargetWiki(); + + /** + * Set the ID of the any foreign wiki to use as a target for database operations + * + * @param string|bool $wiki The target wiki, in a form that LBFactory understands (or false if the local wiki shall be used) + * + * @since 1.20 + */ + public function setTargetWiki( $wiki ); + + /** + * Get the database type used for read operations. + * This is to be used instead of wfGetDB. + * + * @see LoadBalancer::getConnection + * + * @since 1.20 + * + * @return DatabaseBase The database object + */ + public function getReadDbConnection(); + + /** + * Get the database type used for read operations. + * This is to be used instead of wfGetDB. + * + * @see LoadBalancer::getConnection + * + * @since 1.20 + * + * @return DatabaseBase The database object + */ + public function getWriteDbConnection(); + + /** + * Get the database type used for read operations. + * + * @see wfGetLB + * + * @since 1.20 + * + * @return LoadBalancer The database load balancer object + */ + public function getLoadBalancer(); + + /** + * Releases the lease on the given database connection. This is useful mainly + * for connections to a foreign wiki. It does nothing for connections to the local wiki. + * + * @see LoadBalancer::reuseConnection + * + * @param DatabaseBase $db the database + * + * @since 1.20 + */ + public function releaseConnection( DatabaseBase $db ); + + /** * Update the records matching the provided conditions by * setting the fields that are keys in the $values param to * their corresponding values. @@ -381,15 +459,6 @@ interface IORMTable { public function unprefixFieldName( $fieldName ); /** - * Get an instance of this class. - * - * @since 1.20 - * - * @return IORMTable - */ - public static function singleton(); - - /** * Get an array with fields from a database result, * that can be fed directly to the constructor or * to setFields. diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index e82c54ba..16c43a00 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -86,7 +86,7 @@ abstract class LBFactory { * Create a new load balancer object. The resulting object will be untracked, * not chronology-protected, and the caller is responsible for cleaning it up. * - * @param $wiki String: wiki ID, or false for the current wiki + * @param string $wiki wiki ID, or false for the current wiki * @return LoadBalancer */ abstract function newMainLB( $wiki = false ); @@ -94,7 +94,7 @@ abstract class LBFactory { /** * Get a cached (tracked) load balancer object. * - * @param $wiki String: wiki ID, or false for the current wiki + * @param string $wiki wiki ID, or false for the current wiki * @return LoadBalancer */ abstract function getMainLB( $wiki = false ); @@ -104,8 +104,8 @@ abstract class LBFactory { * untracked, not chronology-protected, and the caller is responsible for * cleaning it up. * - * @param $cluster String: external storage cluster, or false for core - * @param $wiki String: wiki ID, or false for the current wiki + * @param string $cluster external storage cluster, or false for core + * @param string $wiki wiki ID, or false for the current wiki * * @return LoadBalancer */ @@ -114,8 +114,8 @@ abstract class LBFactory { /** * Get a cached (tracked) load balancer for external storage * - * @param $cluster String: external storage cluster, or false for core - * @param $wiki String: wiki ID, or false for the current wiki + * @param string $cluster external storage cluster, or false for core + * @param string $wiki wiki ID, or false for the current wiki * * @return LoadBalancer */ @@ -134,7 +134,8 @@ abstract class LBFactory { * Prepare all tracked load balancers for shutdown * STUB */ - function shutdown() {} + function shutdown() { + } /** * Call a method of each tracked load balancer @@ -201,7 +202,7 @@ class LBFactory_Simple extends LBFactory { $flags |= DBO_COMPRESS; } - $servers = array(array( + $servers = array( array( 'host' => $wgDBserver, 'user' => $wgDBuser, 'password' => $wgDBpassword, @@ -240,7 +241,7 @@ class LBFactory_Simple extends LBFactory { function newExternalLB( $cluster, $wiki = false ) { global $wgExternalServers; if ( !isset( $wgExternalServers[$cluster] ) ) { - throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" ); + throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" ); } return new LoadBalancer( array( 'servers' => $wgExternalServers[$cluster] @@ -256,6 +257,7 @@ class LBFactory_Simple extends LBFactory { if ( !isset( $this->extLBs[$cluster] ) ) { $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki ); $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) ); + $this->chronProt->initLB( $this->extLBs[$cluster] ); } return $this->extLBs[$cluster]; } @@ -280,6 +282,9 @@ class LBFactory_Simple extends LBFactory { if ( $this->mainLB ) { $this->chronProt->shutdownLB( $this->mainLB ); } + foreach ( $this->extLBs as $extLB ) { + $this->chronProt->shutdownLB( $extLB ); + } $this->chronProt->shutdown(); $this->commitMasterChanges(); } @@ -292,21 +297,27 @@ class LBFactory_Simple extends LBFactory { * LBFactory::enableBackend() to return to normal behavior */ class LBFactory_Fake extends LBFactory { - function __construct( $conf ) {} + function __construct( $conf ) { + } - function newMainLB( $wiki = false) { + function newMainLB( $wiki = false ) { throw new DBAccessError; } + function getMainLB( $wiki = false ) { throw new DBAccessError; } + function newExternalLB( $cluster, $wiki = false ) { throw new DBAccessError; } + function &getExternalLB( $cluster, $wiki = false ) { throw new DBAccessError; } - function forEachLB( $callback, $params = array() ) {} + + function forEachLB( $callback, $params = array() ) { + } } /** @@ -317,76 +328,3 @@ class DBAccessError extends MWException { parent::__construct( "Mediawiki tried to access the database via wfGetDB(). This is not allowed." ); } } - -/** - * Class for ensuring a consistent ordering of events as seen by the user, despite replication. - * Kind of like Hawking's [[Chronology Protection Agency]]. - */ -class ChronologyProtector { - var $startupPos; - var $shutdownPos = array(); - - /** - * Initialise a LoadBalancer to give it appropriate chronology protection. - * - * @param $lb LoadBalancer - */ - function initLB( $lb ) { - if ( $this->startupPos === null ) { - if ( !empty( $_SESSION[__CLASS__] ) ) { - $this->startupPos = $_SESSION[__CLASS__]; - } - } - if ( !$this->startupPos ) { - return; - } - $masterName = $lb->getServerName( 0 ); - - if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) { - $info = $lb->parentInfo(); - $pos = $this->startupPos[$masterName]; - wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" ); - $lb->waitFor( $this->startupPos[$masterName] ); - } - } - - /** - * Notify the ChronologyProtector that the LoadBalancer is about to shut - * down. Saves replication positions. - * - * @param $lb LoadBalancer - */ - function shutdownLB( $lb ) { - // Don't start a session, don't bother with non-replicated setups - if ( strval( session_id() ) == '' || $lb->getServerCount() <= 1 ) { - return; - } - $masterName = $lb->getServerName( 0 ); - if ( isset( $this->shutdownPos[$masterName] ) ) { - // Already done - return; - } - // Only save the position if writes have been done on the connection - $db = $lb->getAnyOpenConnection( 0 ); - $info = $lb->parentInfo(); - if ( !$db || !$db->doneWrites() ) { - wfDebug( __METHOD__.": LB {$info['id']}, no writes done\n" ); - return; - } - $pos = $db->getMasterPos(); - wfDebug( __METHOD__.": LB {$info['id']} has master pos $pos\n" ); - $this->shutdownPos[$masterName] = $pos; - } - - /** - * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now. - * May commit chronology data to persistent storage. - */ - function shutdown() { - if ( session_id() != '' && count( $this->shutdownPos ) ) { - wfDebug( __METHOD__.": saving master pos for " . - count( $this->shutdownPos ) . " master(s)\n" ); - $_SESSION[__CLASS__] = $this->shutdownPos; - } - } -} diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php index 6008813b..3043946a 100644 --- a/includes/db/LBFactory_Multi.php +++ b/includes/db/LBFactory_Multi.php @@ -21,7 +21,6 @@ * @ingroup Database */ - /** * A multi-wiki, multi-master factory for Wikimedia and similar installations. * Ignores the old configuration globals @@ -70,6 +69,7 @@ class LBFactory_Multi extends LBFactory { /** * @param $conf array + * @throws MWException */ function __construct( $conf ) { $this->chronProt = new ChronologyProtector; @@ -82,7 +82,7 @@ class LBFactory_Multi extends LBFactory { foreach ( $required as $key ) { if ( !isset( $conf[$key] ) ) { - throw new MWException( __CLASS__.": $key is required in configuration" ); + throw new MWException( __CLASS__ . ": $key is required in configuration" ); } $this->$key = $conf[$key]; } @@ -145,21 +145,22 @@ class LBFactory_Multi extends LBFactory { $section = $this->getSectionForWiki( $wiki ); if ( !isset( $this->mainLBs[$section] ) ) { $lb = $this->newMainLB( $wiki, $section ); - $this->chronProt->initLB( $lb ); $lb->parentInfo( array( 'id' => "main-$section" ) ); + $this->chronProt->initLB( $lb ); $this->mainLBs[$section] = $lb; } return $this->mainLBs[$section]; } /** - * @param $cluster - * @param $wiki + * @param string $cluster + * @param bool $wiki + * @throws MWException * @return LoadBalancer */ function newExternalLB( $cluster, $wiki = false ) { if ( !isset( $this->externalLoads[$cluster] ) ) { - throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" ); + throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" ); } $template = $this->serverTemplate; if ( isset( $this->externalTemplateOverrides ) ) { @@ -180,6 +181,7 @@ class LBFactory_Multi extends LBFactory { if ( !isset( $this->extLBs[$cluster] ) ) { $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki ); $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) ); + $this->chronProt->initLB( $this->extLBs[$cluster] ); } return $this->extLBs[$cluster]; } @@ -295,6 +297,9 @@ class LBFactory_Multi extends LBFactory { foreach ( $this->mainLBs as $lb ) { $this->chronProt->shutdownLB( $lb ); } + foreach ( $this->extLBs as $extLB ) { + $this->chronProt->shutdownLB( $extLB ); + } $this->chronProt->shutdown(); $this->commitMasterChanges(); } diff --git a/includes/db/LBFactory_Single.php b/includes/db/LBFactory_Single.php index 4b165b2a..7dca06d7 100644 --- a/includes/db/LBFactory_Single.php +++ b/includes/db/LBFactory_Single.php @@ -28,7 +28,7 @@ class LBFactory_Single extends LBFactory { protected $lb; /** - * @param $conf array An associative array with one member: + * @param array $conf An associative array with one member: * - connection: The DatabaseBase connection object */ function __construct( $conf ) { diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index 0e455e0c..857109db 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -37,14 +37,15 @@ class LoadBalancer { private $mLoadMonitorClass, $mLoadMonitor; /** - * @param $params Array with keys: + * @param array $params with keys: * servers Required. Array of server info structures. * masterWaitTimeout Replication lag wait timeout * loadMonitor Name of a class used to fetch server lag and load. + * @throws MWException */ function __construct( $params ) { if ( !isset( $params['servers'] ) ) { - throw new MWException( __CLASS__.': missing servers parameter' ); + throw new MWException( __CLASS__ . ': missing servers parameter' ); } $this->mServers = $params['servers']; @@ -77,7 +78,7 @@ class LoadBalancer { } } - foreach( $params['servers'] as $i => $server ) { + foreach ( $params['servers'] as $i => $server ) { $this->mLoads[$i] = $server['load']; if ( isset( $server['groupLoads'] ) ) { foreach ( $server['groupLoads'] as $group => $ratio ) { @@ -116,34 +117,14 @@ class LoadBalancer { * Given an array of non-normalised probabilities, this function will select * an element and return the appropriate key * + * @deprecated since 1.21, use ArrayUtils::pickRandom() + * * @param $weights array * - * @return int + * @return bool|int|string */ function pickRandom( $weights ) { - if ( !is_array( $weights ) || count( $weights ) == 0 ) { - return false; - } - - $sum = array_sum( $weights ); - if ( $sum == 0 ) { - # No loads on any of them - # In previous versions, this triggered an unweighted random selection, - # but this feature has been removed as of April 2006 to allow for strict - # separation of query groups. - return false; - } - $max = mt_getrandmax(); - $rand = mt_rand( 0, $max ) / $max * $sum; - - $sum = 0; - foreach ( $weights as $i => $w ) { - $sum += $w; - if ( $sum >= $rand ) { - break; - } - } - return $i; + return ArrayUtils::pickRandom( $weights ); } /** @@ -186,7 +167,7 @@ class LoadBalancer { #wfDebugLog( 'connect', var_export( $loads, true ) ); # Return a random representative of the remainder - return $this->pickRandom( $loads ); + return ArrayUtils::pickRandom( $loads ); } /** @@ -197,17 +178,18 @@ class LoadBalancer { * Side effect: opens connections to databases * @param $group bool * @param $wiki bool + * @throws MWException * @return bool|int|string */ function getReaderIndex( $group = false, $wiki = false ) { global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype; # @todo FIXME: For now, only go through all this for mysql databases - if ($wgDBtype != 'mysql') { + if ( $wgDBtype != 'mysql' ) { return $this->getWriterIndex(); } - if ( count( $this->mServers ) == 1 ) { + if ( count( $this->mServers ) == 1 ) { # Skip the load balancing if there's only one server return 0; } elseif ( $group === false and $this->mReadIndex >= 0 ) { @@ -228,7 +210,7 @@ class LoadBalancer { $nonErrorLoads = $this->mGroupLoads[$group]; } else { # No loads for this group, return false and the caller can use some other group - wfDebug( __METHOD__.": no loads for group $group\n" ); + wfDebug( __METHOD__ . ": no loads for group $group\n" ); wfProfileOut( __METHOD__ ); return false; } @@ -237,6 +219,7 @@ class LoadBalancer { } if ( !$nonErrorLoads ) { + wfProfileOut( __METHOD__ ); throw new MWException( "Empty server array given to LoadBalancer" ); } @@ -253,15 +236,15 @@ class LoadBalancer { $currentLoads = $nonErrorLoads; while ( count( $currentLoads ) ) { if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) { - $i = $this->pickRandom( $currentLoads ); + $i = ArrayUtils::pickRandom( $currentLoads ); } else { $i = $this->getRandomNonLagged( $currentLoads, $wiki ); - if ( $i === false && count( $currentLoads ) != 0 ) { + if ( $i === false && count( $currentLoads ) != 0 ) { # All slaves lagged. Switch to read-only mode wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode\n" ); $wgReadOnly = 'The database has been automatically locked ' . 'while the slave database servers catch up to the master'; - $i = $this->pickRandom( $currentLoads ); + $i = ArrayUtils::pickRandom( $currentLoads ); $laggedSlaveMode = true; } } @@ -270,16 +253,16 @@ class LoadBalancer { # pickRandom() returned false # This is permanent and means the configuration or the load monitor # wants us to return false. - wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" ); + wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false\n" ); wfProfileOut( __METHOD__ ); return false; } - wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" ); + wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: {$this->mServers[$i]['host']}...\n" ); $conn = $this->openConnection( $i, $wiki ); if ( !$conn ) { - wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" ); + wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki\n" ); unset( $nonErrorLoads[$i] ); unset( $currentLoads[$i] ); continue; @@ -318,7 +301,7 @@ class LoadBalancer { # Some servers must have been overloaded if ( $overloadedServers == 0 ) { - throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" ); + throw new MWException( __METHOD__ . ": unexpectedly found no overloaded servers" ); } # Back off for a while # Scale the sleep time by the number of connected threads, to produce a @@ -341,7 +324,7 @@ class LoadBalancer { $this->mServers[$i]['slave pos'] = $conn->getSlavePos(); } } - if ( $this->mReadIndex <=0 && $this->mLoads[$i]>0 && $i !== false ) { + if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $i !== false ) { $this->mReadIndex = $i; } } @@ -356,7 +339,7 @@ class LoadBalancer { */ function sleep( $t ) { wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__.": waiting $t us\n" ); + wfDebug( __METHOD__ . ": waiting $t us\n" ); usleep( $t ); wfProfileOut( __METHOD__ ); return $t; @@ -366,7 +349,7 @@ class LoadBalancer { * Set the master wait position * If a DB_SLAVE connection has been opened already, waits * Otherwise sets a variable telling it to wait if such a connection is opened - * @param $pos int + * @param $pos DBMasterPos */ public function waitFor( $pos ) { wfProfileIn( __METHOD__ ); @@ -384,13 +367,13 @@ class LoadBalancer { /** * Set the master wait position and wait for ALL slaves to catch up to it - * @param $pos int + * @param $pos DBMasterPos */ public function waitForAll( $pos ) { wfProfileIn( __METHOD__ ); $this->mWaitForPos = $pos; for ( $i = 1; $i < count( $this->mServers ); $i++ ) { - $this->doWait( $i , true ); + $this->doWait( $i, true ); } wfProfileOut( __METHOD__ ); } @@ -417,7 +400,7 @@ class LoadBalancer { * @param $open bool * @return bool */ - function doWait( $index, $open = false ) { + protected function doWait( $index, $open = false ) { # Find a connection to wait on $conn = $this->getAnyOpenConnection( $index ); if ( !$conn ) { @@ -425,7 +408,7 @@ class LoadBalancer { wfDebug( __METHOD__ . ": no connection open\n" ); return false; } else { - $conn = $this->openConnection( $index ); + $conn = $this->openConnection( $index, '' ); if ( !$conn ) { wfDebug( __METHOD__ . ": failed to open connection\n" ); return false; @@ -433,15 +416,15 @@ class LoadBalancer { } } - wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" ); + wfDebug( __METHOD__ . ": Waiting for slave #$index to catch up...\n" ); $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout ); if ( $result == -1 || is_null( $result ) ) { # Timed out waiting for slave, use master instead - wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" ); + wfDebug( __METHOD__ . ": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" ); return false; } else { - wfDebug( __METHOD__.": Done\n" ); + wfDebug( __METHOD__ . ": Done\n" ); return true; } } @@ -451,17 +434,20 @@ class LoadBalancer { * This is the main entry point for this class. * * @param $i Integer: server index - * @param $groups Array: query groups - * @param $wiki String: wiki ID + * @param array $groups query groups + * @param bool|string $wiki Wiki ID * + * @throws MWException * @return DatabaseBase */ public function &getConnection( $i, $groups = array(), $wiki = false ) { wfProfileIn( __METHOD__ ); if ( $i == DB_LAST ) { + wfProfileOut( __METHOD__ ); throw new MWException( 'Attempt to call ' . __METHOD__ . ' with deprecated server index DB_LAST' ); } elseif ( $i === null || $i === false ) { + wfProfileOut( __METHOD__ ); throw new MWException( 'Attempt to call ' . __METHOD__ . ' with invalid server index' ); } @@ -476,7 +462,7 @@ class LoadBalancer { $groupIndex = $this->getReaderIndex( $groups, $wiki ); if ( $groupIndex !== false ) { $serverName = $this->getServerName( $groupIndex ); - wfDebug( __METHOD__.": using server $serverName for group $groups\n" ); + wfDebug( __METHOD__ . ": using server $serverName for group $groups\n" ); $i = $groupIndex; } } else { @@ -484,7 +470,7 @@ class LoadBalancer { $groupIndex = $this->getReaderIndex( $group, $wiki ); if ( $groupIndex !== false ) { $serverName = $this->getServerName( $groupIndex ); - wfDebug( __METHOD__.": using server $serverName for group $group\n" ); + wfDebug( __METHOD__ . ": using server $serverName for group $group\n" ); $i = $groupIndex; break; } @@ -493,20 +479,21 @@ class LoadBalancer { # Operation-based index if ( $i == DB_SLAVE ) { + $this->mLastError = 'Unknown error'; // reset error string $i = $this->getReaderIndex( false, $wiki ); # Couldn't find a working server in getReaderIndex()? if ( $i === false ) { $this->mLastError = 'No working slave server: ' . $this->mLastError; - $this->reportConnectionError( $this->mErrorConnection ); wfProfileOut( __METHOD__ ); - return false; + return $this->reportConnectionError(); } } # Now we have an explicit index into the servers array $conn = $this->openConnection( $i, $wiki ); if ( !$conn ) { - $this->reportConnectionError( $this->mErrorConnection ); + wfProfileOut( __METHOD__ ); + return $this->reportConnectionError(); } wfProfileOut( __METHOD__ ); @@ -519,10 +506,11 @@ class LoadBalancer { * the same number of times as getConnection() to work. * * @param DatabaseBase $conn + * @throws MWException */ public function reuseConnection( $conn ) { - $serverIndex = $conn->getLBInfo('serverIndex'); - $refCount = $conn->getLBInfo('foreignPoolRefCount'); + $serverIndex = $conn->getLBInfo( 'serverIndex' ); + $refCount = $conn->getLBInfo( 'foreignPoolRefCount' ); $dbName = $conn->getDBname(); $prefix = $conn->tablePrefix(); if ( strval( $prefix ) !== '' ) { @@ -531,7 +519,7 @@ class LoadBalancer { $wiki = $dbName; } if ( $serverIndex === null || $refCount === null ) { - wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" ); + wfDebug( __METHOD__ . ": this connection was not opened as a foreign connection\n" ); /** * This can happen in code like: * foreach ( $dbs as $db ) { @@ -545,19 +533,51 @@ class LoadBalancer { return; } if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) { - throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" ); + throw new MWException( __METHOD__ . ": connection not found, has the connection been freed already?" ); } $conn->setLBInfo( 'foreignPoolRefCount', --$refCount ); if ( $refCount <= 0 ) { $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn; unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] ); - wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" ); + wfDebug( __METHOD__ . ": freed connection $serverIndex/$wiki\n" ); } else { - wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" ); + wfDebug( __METHOD__ . ": reference count for $serverIndex/$wiki reduced to $refCount\n" ); } } /** + * Get a database connection handle reference + * + * The handle's methods wrap simply wrap those of a DatabaseBase handle + * + * @see LoadBalancer::getConnection() for parameter information + * + * @param integer $db + * @param mixed $groups + * @param string $wiki + * @return DBConnRef + */ + public function getConnectionRef( $db, $groups = array(), $wiki = false ) { + return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) ); + } + + /** + * Get a database connection handle reference without connecting yet + * + * The handle's methods wrap simply wrap those of a DatabaseBase handle + * + * @see LoadBalancer::getConnection() for parameter information + * + * @param integer $db + * @param mixed $groups + * @param string $wiki + * @return DBConnRef + */ + public function getLazyConnectionRef( $db, $groups = array(), $wiki = false ) { + return new DBConnRef( $this, array( $db, $groups, $wiki ) ); + } + + /** * Open a connection to the server given by the specified index * Index must be an actual index into the array. * If the server is already open, returns it. @@ -566,7 +586,7 @@ class LoadBalancer { * error will be available via $this->mErrorConnection. * * @param $i Integer server index - * @param $wiki String wiki ID to open + * @param string $wiki wiki ID to open * @return DatabaseBase * * @access private @@ -575,7 +595,7 @@ class LoadBalancer { wfProfileIn( __METHOD__ ); if ( $wiki !== false ) { $conn = $this->openForeignConnection( $i, $wiki ); - wfProfileOut( __METHOD__); + wfProfileOut( __METHOD__ ); return $conn; } if ( isset( $this->mConns['local'][$i][0] ) ) { @@ -585,6 +605,7 @@ class LoadBalancer { $server['serverIndex'] = $i; $conn = $this->reallyOpenConnection( $server, false ); if ( $conn->isOpen() ) { + wfDebug( "Connected to database $i at {$this->mServers[$i]['host']}\n" ); $this->mConns['local'][$i][0] = $conn; } else { wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" ); @@ -611,22 +632,22 @@ class LoadBalancer { * error will be available via $this->mErrorConnection. * * @param $i Integer: server index - * @param $wiki String: wiki ID to open + * @param string $wiki wiki ID to open * @return DatabaseBase */ function openForeignConnection( $i, $wiki ) { - wfProfileIn(__METHOD__); + wfProfileIn( __METHOD__ ); list( $dbName, $prefix ) = wfSplitWikiID( $wiki ); if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) { // Reuse an already-used connection $conn = $this->mConns['foreignUsed'][$i][$wiki]; - wfDebug( __METHOD__.": reusing connection $i/$wiki\n" ); + wfDebug( __METHOD__ . ": reusing connection $i/$wiki\n" ); } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) { // Reuse a free connection for the same wiki $conn = $this->mConns['foreignFree'][$i][$wiki]; unset( $this->mConns['foreignFree'][$i][$wiki] ); $this->mConns['foreignUsed'][$i][$wiki] = $conn; - wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" ); + wfDebug( __METHOD__ . ": reusing free connection $i/$wiki\n" ); } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) { // Reuse a connection from another wiki $conn = reset( $this->mConns['foreignFree'][$i] ); @@ -641,22 +662,23 @@ class LoadBalancer { $conn->tablePrefix( $prefix ); unset( $this->mConns['foreignFree'][$i][$oldWiki] ); $this->mConns['foreignUsed'][$i][$wiki] = $conn; - wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" ); + wfDebug( __METHOD__ . ": reusing free connection from $oldWiki for $wiki\n" ); } } else { // Open a new connection $server = $this->mServers[$i]; $server['serverIndex'] = $i; $server['foreignPoolRefCount'] = 0; + $server['foreign'] = true; $conn = $this->reallyOpenConnection( $server, $dbName ); if ( !$conn->isOpen() ) { - wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" ); + wfDebug( __METHOD__ . ": error opening connection for $i/$wiki\n" ); $this->mErrorConnection = $conn; $conn = false; } else { $conn->tablePrefix( $prefix ); $this->mConns['foreignUsed'][$i][$wiki] = $conn; - wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" ); + wfDebug( __METHOD__ . ": opened new connection for $i/$wiki\n" ); } } @@ -665,7 +687,7 @@ class LoadBalancer { $refCount = $conn->getLBInfo( 'foreignPoolRefCount' ); $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 ); } - wfProfileOut(__METHOD__); + wfProfileOut( __METHOD__ ); return $conn; } @@ -677,7 +699,7 @@ class LoadBalancer { * @return bool */ function isOpen( $index ) { - if( !is_integer( $index ) ) { + if ( !is_integer( $index ) ) { return false; } return (bool)$this->getAnyOpenConnection( $index ); @@ -690,23 +712,20 @@ class LoadBalancer { * * @param $server * @param $dbNameOverride bool + * @throws MWException * @return DatabaseBase */ function reallyOpenConnection( $server, $dbNameOverride = false ) { - if( !is_array( $server ) ) { + if ( !is_array( $server ) ) { throw new MWException( 'You must update your load-balancing configuration. ' . 'See DefaultSettings.php entry for $wgDBservers.' ); } - $host = $server['host']; - $dbname = $server['dbname']; - if ( $dbNameOverride !== false ) { - $server['dbname'] = $dbname = $dbNameOverride; + $server['dbname'] = $dbNameOverride; } # Create object - wfDebug( "Connecting to $host $dbname...\n" ); try { $db = DatabaseBase::factory( $server['type'], $server ); } catch ( DBConnectionError $e ) { @@ -715,11 +734,6 @@ class LoadBalancer { $db = $e->db; } - if ( $db->isOpen() ) { - wfDebug( "Connected to $host $dbname.\n" ); - } else { - wfDebug( "Connection failed to $host $dbname.\n" ); - } $db->setLBInfo( $server ); if ( isset( $server['fakeSlaveLag'] ) ) { $db->setFakeSlaveLag( $server['fakeSlaveLag'] ); @@ -731,24 +745,24 @@ class LoadBalancer { } /** - * @param $conn * @throws DBConnectionError + * @return bool */ - function reportConnectionError( &$conn ) { - wfProfileIn( __METHOD__ ); + private function reportConnectionError() { + $conn = $this->mErrorConnection; // The connection which caused the error if ( !is_object( $conn ) ) { // No last connection, probably due to all servers being too busy wfLogDBError( "LB failure with no last connection. Connection error: {$this->mLastError}\n" ); - $conn = new Database; + // If all servers were busy, mLastError will contain something sensible - throw new DBConnectionError( $conn, $this->mLastError ); + throw new DBConnectionError( null, $this->mLastError ); } else { $server = $conn->getProperty( 'mServer' ); wfLogDBError( "Connection error: {$this->mLastError} ({$server})\n" ); - $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); + $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); // throws DBConnectionError } - wfProfileOut( __METHOD__ ); + return false; /* not reached */ } /** @@ -853,7 +867,7 @@ class LoadBalancer { */ function closeAll() { foreach ( $this->mConns as $conns2 ) { - foreach ( $conns2 as $conns3 ) { + foreach ( $conns2 as $conns3 ) { foreach ( $conns3 as $conn ) { $conn->close(); } @@ -909,7 +923,9 @@ class LoadBalancer { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { foreach ( $conns3 as $conn ) { - $conn->commit( __METHOD__ ); + if ( $conn->trxLevel() ) { + $conn->commit( __METHOD__, 'flush' ); + } } } } @@ -926,8 +942,8 @@ class LoadBalancer { continue; } foreach ( $conns2[$masterIndex] as $conn ) { - if ( $conn->writesOrCallbacksPending() ) { - $conn->commit( __METHOD__ ); + if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) { + $conn->commit( __METHOD__, 'flush' ); } } } @@ -954,10 +970,11 @@ class LoadBalancer { * @return bool */ function allowLagged( $mode = null ) { - if ( $mode === null) { + if ( $mode === null ) { return $this->mAllowLagged; } $this->mAllowLagged = $mode; + return $this->mAllowLagged; } /** @@ -999,7 +1016,7 @@ class LoadBalancer { * May attempt to open connections to slaves on the default DB. If there is * no lag, the maximum lag will be reported as -1. * - * @param $wiki string Wiki ID, or false for the default database + * @param string $wiki Wiki ID, or false for the default database * * @return array ( host, max lag, index of max lagged host ) */ @@ -1084,3 +1101,46 @@ class LoadBalancer { $this->mLagTimes = null; } } + +/** + * Helper class to handle automatically marking connectons as reusable (via RAII pattern) + * as well handling deferring the actual network connection until the handle is used + * + * @ingroup Database + * @since 1.22 + */ +class DBConnRef implements IDatabase { + /** @var LoadBalancer */ + protected $lb; + /** @var DatabaseBase|null */ + protected $conn; + /** @var Array|null */ + protected $params; + + /** + * @param $lb LoadBalancer + * @param $conn DatabaseBase|array Connection or (server index, group, wiki ID) array + */ + public function __construct( LoadBalancer $lb, $conn ) { + $this->lb = $lb; + if ( $conn instanceof DatabaseBase ) { + $this->conn = $conn; + } else { + $this->params = $conn; + } + } + + public function __call( $name, $arguments ) { + if ( $this->conn === null ) { + list( $db, $groups, $wiki ) = $this->params; + $this->conn = $this->lb->getConnection( $db, $groups, $wiki ); + } + return call_user_func_array( array( $this->conn, $name ), $arguments ); + } + + function __destruct() { + if ( $this->conn !== null ) { + $this->lb->reuseConnection( $this->conn ); + } + } +} diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 146ac61e..519e2dfd 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -37,7 +37,7 @@ interface LoadMonitor { /** * Perform pre-connection load ratio adjustment. * @param $loads array - * @param $group String: the selected query group + * @param string $group the selected query group * @param $wiki String */ function scaleLoads( &$loads, $group = false, $wiki = false ); @@ -135,18 +135,19 @@ class LoadMonitor_MySQL implements LoadMonitor { $requestRate = 10; global $wgMemc; - if ( empty( $wgMemc ) ) + if ( empty( $wgMemc ) ) { $wgMemc = wfGetMainCache(); + } $masterName = $this->parent->getServerName( 0 ); $memcKey = wfMemcKey( 'lag_times', $masterName ); $times = $wgMemc->get( $memcKey ); - if ( $times ) { + if ( is_array( $times ) ) { # Randomly recache with probability rising over $expiry $elapsed = time() - $times['timestamp']; $chance = max( 0, ( $expiry - $elapsed ) * $requestRate ); if ( mt_rand( 0, $chance ) != 0 ) { - unset( $times['timestamp'] ); + unset( $times['timestamp'] ); // hide from caller wfProfileOut( __METHOD__ ); return $times; } @@ -156,10 +157,21 @@ class LoadMonitor_MySQL implements LoadMonitor { } # Cache key missing or expired + if ( $wgMemc->add( "$memcKey:lock", 1, 10 ) ) { + # Let this process alone update the cache value + $unlocker = new ScopedCallback( function() use ( $wgMemc, $memcKey ) { + $wgMemc->delete( $memcKey ); + } ); + } elseif ( is_array( $times ) ) { + # Could not acquire lock but an old cache exists, so use it + unset( $times['timestamp'] ); // hide from caller + wfProfileOut( __METHOD__ ); + return $times; + } $times = array(); foreach ( $serverIndexes as $i ) { - if ($i == 0) { # Master + if ( $i == 0 ) { # Master $times[$i] = 0; } elseif ( false !== ( $conn = $this->parent->getAnyOpenConnection( $i ) ) ) { $times[$i] = $conn->getLag(); @@ -170,14 +182,11 @@ class LoadMonitor_MySQL implements LoadMonitor { # Add a timestamp key so we know when it was cached $times['timestamp'] = time(); - $wgMemc->set( $memcKey, $times, $expiry ); - - # But don't give the timestamp to the caller - unset($times['timestamp']); - $lagTimes = $times; + $wgMemc->set( $memcKey, $times, $expiry + 10 ); + unset( $times['timestamp'] ); // hide from caller wfProfileOut( __METHOD__ ); - return $lagTimes; + return $times; } /** @@ -189,7 +198,7 @@ class LoadMonitor_MySQL implements LoadMonitor { if ( !$threshold ) { return 0; } - $status = $conn->getMysqlStatus("Thread%"); + $status = $conn->getMysqlStatus( "Thread%" ); if ( $status['Threads_running'] > $threshold ) { $server = $conn->getProperty( 'mServer' ); wfLogDBError( "LB backoff from $server - Threads_running = {$status['Threads_running']}\n" ); @@ -199,4 +208,3 @@ class LoadMonitor_MySQL implements LoadMonitor { } } } - diff --git a/includes/db/ORMIterator.php b/includes/db/ORMIterator.php index 090b8932..077eab0f 100644 --- a/includes/db/ORMIterator.php +++ b/includes/db/ORMIterator.php @@ -23,9 +23,9 @@ * @file * @ingroup ORM * - * @licence GNU GPL v2 or later + * @license GNU GPL v2 or later * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ interface ORMIterator extends Iterator { -}
\ No newline at end of file +} diff --git a/includes/db/ORMResult.php b/includes/db/ORMResult.php index 2a5837a1..160033c4 100644 --- a/includes/db/ORMResult.php +++ b/includes/db/ORMResult.php @@ -25,7 +25,7 @@ * @file ORMResult.php * @ingroup ORM * - * @licence GNU GPL v2 or later + * @license GNU GPL v2 or later * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ diff --git a/includes/db/ORMRow.php b/includes/db/ORMRow.php index 303f3a20..5ce3794d 100644 --- a/includes/db/ORMRow.php +++ b/includes/db/ORMRow.php @@ -27,11 +27,11 @@ * @file ORMRow.php * @ingroup ORM * - * @licence GNU GPL v2 or later + * @license GNU GPL v2 or later * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ -abstract class ORMRow implements IORMRow { +class ORMRow implements IORMRow { /** * The fields of the object. @@ -43,17 +43,12 @@ abstract class ORMRow implements IORMRow { protected $fields = array( 'id' => null ); /** - * @since 1.20 - * @var ORMTable - */ - protected $table; - - /** * If the object should update summaries of linked items when changed. * For example, update the course_count field in universities when a course in courses is deleted. * Settings this to false can prevent needless updating work in situations * such as deleting a university, which will then delete all it's courses. * + * @deprecated since 1.22 * @since 1.20 * @var bool */ @@ -64,21 +59,29 @@ abstract class ORMRow implements IORMRow { * This mode indicates that only summary fields got updated, * which allows for optimizations. * + * @deprecated since 1.22 * @since 1.20 * @var bool */ protected $inSummaryMode = false; /** + * @deprecated since 1.22 + * @since 1.20 + * @var ORMTable|null + */ + protected $table; + + /** * Constructor. * * @since 1.20 * - * @param IORMTable $table + * @param IORMTable|null $table Deprecated since 1.22 * @param array|null $fields - * @param boolean $loadDefaults + * @param boolean $loadDefaults Deprecated since 1.22 */ - public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ) { + public function __construct( IORMTable $table = null, $fields = null, $loadDefaults = false ) { $this->table = $table; if ( !is_array( $fields ) ) { @@ -96,6 +99,7 @@ abstract class ORMRow implements IORMRow { * Load the specified fields from the database. * * @since 1.20 + * @deprecated since 1.22 * * @param array|null $fields * @param boolean $override @@ -120,7 +124,8 @@ abstract class ORMRow implements IORMRow { $result = $this->table->rawSelectRow( $this->table->getPrefixedFields( $fields ), array( $this->table->getPrefixedField( 'id' ) => $this->getId() ), - array( 'LIMIT' => 1 ) + array( 'LIMIT' => 1 ), + __METHOD__ ); if ( $result !== false ) { @@ -138,8 +143,9 @@ abstract class ORMRow implements IORMRow { * * @since 1.20 * - * @param string $name - * @param mixed $default + * @param string $name Field name + * @param $default mixed: Default value to return when none is found + * (default: null) * * @throws MWException * @return mixed @@ -158,8 +164,9 @@ abstract class ORMRow implements IORMRow { * Gets the value of a field but first loads it if not done so already. * * @since 1.20 + * @deprecated since 1.22 * - * @param string$name + * @param $name string * * @return mixed */ @@ -230,25 +237,10 @@ abstract class ORMRow implements IORMRow { } /** - * Sets multiple fields. - * - * @since 1.20 - * - * @param array $fields The fields to set - * @param boolean $override Override already set fields with the provided values? - */ - public function setFields( array $fields, $override = true ) { - foreach ( $fields as $name => $value ) { - if ( $override || !$this->hasField( $name ) ) { - $this->setField( $name, $value ); - } - } - } - - /** * Gets the fields => values to write to the table. * * @since 1.20 + * @deprecated since 1.22 * * @return array */ @@ -259,11 +251,18 @@ abstract class ORMRow implements IORMRow { if ( array_key_exists( $name, $this->fields ) ) { $value = $this->fields[$name]; + // Skip null id fields so that the DBMS can set the default. + if ( $name === 'id' && is_null ( $value ) ) { + continue; + } + switch ( $type ) { case 'array': $value = (array)$value; + // fall-through! case 'blob': $value = serialize( $value ); + // fall-through! } $values[$this->table->getPrefixedField( $name )] = $value; @@ -274,6 +273,22 @@ abstract class ORMRow implements IORMRow { } /** + * Sets multiple fields. + * + * @since 1.20 + * + * @param array $fields The fields to set + * @param boolean $override Override already set fields with the provided values? + */ + public function setFields( array $fields, $override = true ) { + foreach ( $fields as $name => $value ) { + if ( $override || !$this->hasField( $name ) ) { + $this->setField( $name, $value ); + } + } + } + + /** * Serializes the object to an associative array which * can then easily be converted into JSON or similar. * @@ -311,6 +326,7 @@ abstract class ORMRow implements IORMRow { * Load the default values, via getDefaults. * * @since 1.20 + * @deprecated since 1.22 * * @param boolean $override */ @@ -323,6 +339,7 @@ abstract class ORMRow implements IORMRow { * when it already exists, or inserting it when it doesn't. * * @since 1.20 + * @deprecated since 1.22 Use IORMTable->updateRow or ->insertRow * * @param string|null $functionName * @@ -330,9 +347,9 @@ abstract class ORMRow implements IORMRow { */ public function save( $functionName = null ) { if ( $this->hasIdField() ) { - return $this->saveExisting( $functionName ); + return $this->table->updateRow( $this, $functionName ); } else { - return $this->insert( $functionName ); + return $this->table->insertRow( $this, $functionName ); } } @@ -340,13 +357,14 @@ abstract class ORMRow implements IORMRow { * Updates the object in the database. * * @since 1.20 + * @deprecated since 1.22 * * @param string|null $functionName * * @return boolean Success indicator */ protected function saveExisting( $functionName = null ) { - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->table->getWriteDbConnection(); $success = $dbw->update( $this->table->getName(), @@ -355,6 +373,8 @@ abstract class ORMRow implements IORMRow { is_null( $functionName ) ? __METHOD__ : $functionName ); + $this->table->releaseConnection( $dbw ); + // DatabaseBase::update does not always return true for success as documented... return $success !== false; } @@ -375,6 +395,7 @@ abstract class ORMRow implements IORMRow { * Inserts the object into the database. * * @since 1.20 + * @deprecated since 1.22 * * @param string|null $functionName * @param array|null $options @@ -382,13 +403,13 @@ abstract class ORMRow implements IORMRow { * @return boolean Success indicator */ protected function insert( $functionName = null, array $options = null ) { - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->table->getWriteDbConnection(); $success = $dbw->insert( $this->table->getName(), $this->getWriteValues(), is_null( $functionName ) ? __METHOD__ : $functionName, - is_null( $options ) ? array( 'IGNORE' ) : $options + $options ); // DatabaseBase::insert does not always return true for success as documented... @@ -398,6 +419,8 @@ abstract class ORMRow implements IORMRow { $this->setField( 'id', $dbw->insertId() ); } + $this->table->releaseConnection( $dbw ); + return $success; } @@ -405,16 +428,14 @@ abstract class ORMRow implements IORMRow { * Removes the object from the database. * * @since 1.20 + * @deprecated since 1.22, use IORMTable->removeRow * * @return boolean Success indicator */ public function remove() { $this->beforeRemove(); - $success = $this->table->delete( array( 'id' => $this->getId() ) ); - - // DatabaseBase::delete does not always return true for success as documented... - $success = $success !== false; + $success = $this->table->removeRow( $this, __METHOD__ ); if ( $success ) { $this->onRemoved(); @@ -427,6 +448,7 @@ abstract class ORMRow implements IORMRow { * Gets called before an object is removed from the database. * * @since 1.20 + * @deprecated since 1.22 */ protected function beforeRemove() { $this->loadFields( $this->getBeforeRemoveFields(), false, true ); @@ -446,10 +468,11 @@ abstract class ORMRow implements IORMRow { } /** - * Gets called after successfull removal. - * Can be overriden to get rid of linked data. + * Gets called after successful removal. + * Can be overridden to get rid of linked data. * * @since 1.20 + * @deprecated since 1.22 */ protected function onRemoved() { $this->setField( 'id', null ); @@ -490,55 +513,14 @@ abstract class ORMRow implements IORMRow { * @throws MWException */ public function setField( $name, $value ) { - $fields = $this->table->getFields(); - - if ( array_key_exists( $name, $fields ) ) { - switch ( $fields[$name] ) { - case 'int': - $value = (int)$value; - break; - case 'float': - $value = (float)$value; - break; - case 'bool': - if ( is_string( $value ) ) { - $value = $value !== '0'; - } elseif ( is_int( $value ) ) { - $value = $value !== 0; - } - break; - case 'array': - if ( is_string( $value ) ) { - $value = unserialize( $value ); - } - - if ( !is_array( $value ) ) { - $value = array(); - } - break; - case 'blob': - if ( is_string( $value ) ) { - $value = unserialize( $value ); - } - break; - case 'id': - if ( is_string( $value ) ) { - $value = (int)$value; - } - break; - } - - $this->fields[$name] = $value; - } else { - throw new MWException( 'Attempted to set unknown field ' . $name ); - } + $this->fields[$name] = $value; } /** * Add an amount (can be negative) to the specified field (needs to be numeric). - * TODO: most off this stuff makes more sense in the table class * * @since 1.20 + * @deprecated since 1.22, use IORMTable->addToField * * @param string $field * @param integer $amount @@ -546,39 +528,14 @@ abstract class ORMRow implements IORMRow { * @return boolean Success indicator */ public function addToField( $field, $amount ) { - if ( $amount == 0 ) { - return true; - } - - if ( !$this->hasIdField() ) { - return false; - } - - $absoluteAmount = abs( $amount ); - $isNegative = $amount < 0; - - $dbw = wfGetDB( DB_MASTER ); - - $fullField = $this->table->getPrefixedField( $field ); - - $success = $dbw->update( - $this->table->getName(), - array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ), - array( $this->table->getPrefixedField( 'id' ) => $this->getId() ), - __METHOD__ - ); - - if ( $success && $this->hasField( $field ) ) { - $this->setField( $field, $this->getField( $field ) + $amount ); - } - - return $success; + return $this->table->addToField( $this->getUpdateConditions(), $field, $amount ); } /** * Return the names of the fields. * * @since 1.20 + * @deprecated since 1.22 * * @return array */ @@ -590,6 +547,7 @@ abstract class ORMRow implements IORMRow { * Computes and updates the values of the summary fields. * * @since 1.20 + * @deprecated since 1.22 * * @param array|string|null $summaryFields */ @@ -601,6 +559,7 @@ abstract class ORMRow implements IORMRow { * Sets the value for the @see $updateSummaries field. * * @since 1.20 + * @deprecated since 1.22 * * @param boolean $update */ @@ -612,6 +571,7 @@ abstract class ORMRow implements IORMRow { * Sets the value for the @see $inSummaryMode field. * * @since 1.20 + * @deprecated since 1.22 * * @param boolean $summaryMode */ @@ -620,39 +580,10 @@ abstract class ORMRow implements IORMRow { } /** - * Return if any fields got changed. - * - * @since 1.20 - * - * @param IORMRow $object - * @param boolean|array $excludeSummaryFields - * When set to true, summary field changes are ignored. - * Can also be an array of fields to ignore. - * - * @return boolean - */ - protected function fieldsChanged( IORMRow $object, $excludeSummaryFields = false ) { - $exclusionFields = array(); - - if ( $excludeSummaryFields !== false ) { - $exclusionFields = is_array( $excludeSummaryFields ) ? $excludeSummaryFields : $this->table->getSummaryFields(); - } - - foreach ( $this->fields as $name => $value ) { - $excluded = $excludeSummaryFields && in_array( $name, $exclusionFields ); - - if ( !$excluded && $object->getField( $name ) !== $value ) { - return true; - } - } - - return false; - } - - /** * Returns the table this IORMRow is a row in. * * @since 1.20 + * @deprecated since 1.22 * * @return IORMTable */ diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php index a77074ff..5f6723b9 100644 --- a/includes/db/ORMTable.php +++ b/includes/db/ORMTable.php @@ -19,43 +19,150 @@ * http://www.gnu.org/copyleft/gpl.html * * @since 1.20 + * Non-abstract since 1.21 * * @file ORMTable.php * @ingroup ORM * - * @licence GNU GPL v2 or later + * @license GNU GPL v2 or later * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ -abstract class ORMTable implements IORMTable { +class ORMTable extends DBAccessBase implements IORMTable { /** - * Gets the db field prefix. + * Cache for instances, used by the singleton method. * * @since 1.20 + * @deprecated since 1.21 * - * @return string + * @var ORMTable[] */ - protected abstract function getFieldPrefix(); + protected static $instanceCache = array(); /** - * Cache for instances, used by the singleton method. + * @since 1.21 * - * @since 1.20 - * @var array of DBTable + * @var string */ - protected static $instanceCache = array(); + protected $tableName; + + /** + * @since 1.21 + * + * @var string[] + */ + protected $fields = array(); + + /** + * @since 1.21 + * + * @var string + */ + protected $fieldPrefix = ''; + + /** + * @since 1.21 + * + * @var string + */ + protected $rowClass = 'ORMRow'; + + /** + * @since 1.21 + * + * @var array + */ + protected $defaults = array(); /** - * The database connection to use for read operations. + * ID of the database connection to use for read operations. * Can be changed via @see setReadDb. * * @since 1.20 + * * @var integer DB_ enum */ protected $readDb = DB_SLAVE; /** + * Constructor. + * + * @since 1.21 + * + * @param string $tableName + * @param string[] $fields + * @param array $defaults + * @param string|null $rowClass + * @param string $fieldPrefix + */ + public function __construct( $tableName = '', array $fields = array(), array $defaults = array(), $rowClass = null, $fieldPrefix = '' ) { + $this->tableName = $tableName; + $this->fields = $fields; + $this->defaults = $defaults; + + if ( is_string( $rowClass ) ) { + $this->rowClass = $rowClass; + } + + $this->fieldPrefix = $fieldPrefix; + } + + /** + * @see IORMTable::getName + * + * @since 1.21 + * + * @return string + * @throws MWException + */ + public function getName() { + if ( $this->tableName === '' ) { + throw new MWException( 'The table name needs to be set' ); + } + + return $this->tableName; + } + + /** + * Gets the db field prefix. + * + * @since 1.20 + * + * @return string + */ + protected function getFieldPrefix() { + return $this->fieldPrefix; + } + + /** + * @see IORMTable::getRowClass + * + * @since 1.21 + * + * @return string + */ + public function getRowClass() { + return $this->rowClass; + } + + /** + * @see ORMTable::getFields + * + * @since 1.21 + * + * @return array + * @throws MWException + */ + public function getFields() { + if ( $this->fields === array() ) { + throw new MWException( 'The table needs to have one or more fields' ); + } + + return $this->fields; + } + + /** * Returns a list of default field values. * field name => field value * @@ -64,7 +171,7 @@ abstract class ORMTable implements IORMTable { * @return array */ public function getDefaults() { - return array(); + return $this->defaults; } /** @@ -94,8 +201,9 @@ abstract class ORMTable implements IORMTable { * @return ORMResult */ public function select( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ) { - return new ORMResult( $this, $this->rawSelect( $fields, $conditions, $options, $functionName ) ); + array $options = array(), $functionName = null ) { + $res = $this->rawSelect( $fields, $conditions, $options, $functionName ); + return new ORMResult( $this, $res ); } /** @@ -109,10 +217,11 @@ abstract class ORMTable implements IORMTable { * @param array $options * @param string|null $functionName * - * @return array of self + * @return array of row objects + * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode). */ public function selectObjects( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ) { + array $options = array(), $functionName = null ) { $result = $this->selectFields( $fields, $conditions, $options, false, $functionName ); $objects = array(); @@ -130,14 +239,15 @@ abstract class ORMTable implements IORMTable { * @since 1.20 * * @param null|string|array $fields - * @param array $conditions - * @param array $options - * @param null|string $functionName + * @param array $conditions + * @param array $options + * @param null|string $functionName * * @return ResultWrapper + * @throws DBQueryError if the quey failed (even if the database was in ignoreErrors mode). */ public function rawSelect( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ) { + array $options = array(), $functionName = null ) { if ( is_null( $fields ) ) { $fields = array_keys( $this->getFields() ); } @@ -145,13 +255,39 @@ abstract class ORMTable implements IORMTable { $fields = (array)$fields; } - return wfGetDB( $this->getReadDb() )->select( + $dbr = $this->getReadDbConnection(); + $result = $dbr->select( $this->getName(), $this->getPrefixedFields( $fields ), $this->getPrefixedValues( $conditions ), is_null( $functionName ) ? __METHOD__ : $functionName, $options ); + + /* @var Exception $error */ + $error = null; + + if ( $result === false ) { + // Database connection was in "ignoreErrors" mode. We don't like that. + // So, we emulate the DBQueryError that should have been thrown. + $error = new DBQueryError( + $dbr, + $dbr->lastError(), + $dbr->lastErrno(), + $dbr->lastQuery(), + is_null( $functionName ) ? __METHOD__ : $functionName + ); + } + + $this->releaseConnection( $dbr ); + + if ( $error ) { + // Note: construct the error before releasing the connection, + // but throw it after. + throw $error; + } + + return $result; } /** @@ -177,7 +313,7 @@ abstract class ORMTable implements IORMTable { * @return array of array */ public function selectFields( $fields = null, array $conditions = array(), - array $options = array(), $collapse = true, $functionName = null ) { + array $options = array(), $collapse = true, $functionName = null ) { $objects = array(); $result = $this->rawSelect( $fields, $conditions, $options, $functionName ); @@ -223,7 +359,7 @@ abstract class ORMTable implements IORMTable { $objects = $this->select( $fields, $conditions, $options, $functionName ); - return $objects->isEmpty() ? false : $objects->current(); + return ( !$objects || $objects->isEmpty() ) ? false : $objects->current(); } /** @@ -241,15 +377,18 @@ abstract class ORMTable implements IORMTable { */ public function rawSelectRow( array $fields, array $conditions = array(), array $options = array(), $functionName = null ) { - $dbr = wfGetDB( $this->getReadDb() ); + $dbr = $this->getReadDbConnection(); - return $dbr->selectRow( + $result = $dbr->selectRow( $this->getName(), $fields, $conditions, is_null( $functionName ) ? __METHOD__ : $functionName, $options ); + + $this->releaseConnection( $dbr ); + return $result; } /** @@ -293,6 +432,21 @@ abstract class ORMTable implements IORMTable { } /** + * Checks if the table exists + * + * @since 1.21 + * + * @return boolean + */ + public function exists() { + $dbr = $this->getReadDbConnection(); + $exists = $dbr->tableExists( $this->getName() ); + $this->releaseConnection( $dbr ); + + return $exists; + } + + /** * Returns the amount of matching records. * Condition field names get prefixed. * @@ -310,7 +464,8 @@ abstract class ORMTable implements IORMTable { $res = $this->rawSelectRow( array( 'rowcount' => 'COUNT(*)' ), $this->getPrefixedValues( $conditions ), - $options + $options, + __METHOD__ ); return $res->rowcount; @@ -327,13 +482,18 @@ abstract class ORMTable implements IORMTable { * @return boolean Success indicator */ public function delete( array $conditions, $functionName = null ) { - return wfGetDB( DB_MASTER )->delete( + $dbw = $this->getWriteDbConnection(); + + $result = $dbw->delete( $this->getName(), $conditions === array() ? '*' : $this->getPrefixedValues( $conditions ), - $functionName + is_null( $functionName ) ? __METHOD__ : $functionName ) !== false; // DatabaseBase::delete does not always return true for success as documented... + + $this->releaseConnection( $dbw ); + return $result; } - + /** * Get API parameters for the fields supported by this object. * @@ -397,7 +557,7 @@ abstract class ORMTable implements IORMTable { } /** - * Get the database type used for read operations. + * Get the database ID used for read operations. * * @since 1.20 * @@ -408,7 +568,7 @@ abstract class ORMTable implements IORMTable { } /** - * Set the database type to use for read operations. + * Set the database ID to use for read operations, use DB_XXX constants or an index to the load balancer setup. * * @param integer $db * @@ -419,6 +579,70 @@ abstract class ORMTable implements IORMTable { } /** + * Get the ID of the any foreign wiki to use as a target for database operations + * + * @since 1.20 + * + * @return String|bool The target wiki, in a form that LBFactory understands (or false if the local wiki is used) + */ + public function getTargetWiki() { + return $this->wiki; + } + + /** + * Set the ID of the any foreign wiki to use as a target for database operations + * + * @param string|bool $wiki The target wiki, in a form that LBFactory understands (or false if the local wiki shall be used) + * + * @since 1.20 + */ + public function setTargetWiki( $wiki ) { + $this->wiki = $wiki; + } + + /** + * Get the database type used for read operations. + * This is to be used instead of wfGetDB. + * + * @see LoadBalancer::getConnection + * + * @since 1.20 + * + * @return DatabaseBase The database object + */ + public function getReadDbConnection() { + return $this->getConnection( $this->getReadDb(), array() ); + } + + /** + * Get the database type used for read operations. + * This is to be used instead of wfGetDB. + * + * @see LoadBalancer::getConnection + * + * @since 1.20 + * + * @return DatabaseBase The database object + */ + public function getWriteDbConnection() { + return $this->getConnection( DB_MASTER, array() ); + } + + /** + * Releases the lease on the given database connection. This is useful mainly + * for connections to a foreign wiki. It does nothing for connections to the local wiki. + * + * @see LoadBalancer::reuseConnection + * + * @param DatabaseBase $db the database + * + * @since 1.20 + */ + public function releaseConnection( DatabaseBase $db ) { + parent::releaseConnection( $db ); // just make it public + } + + /** * Update the records matching the provided conditions by * setting the fields that are keys in the $values param to * their corresponding values. @@ -431,14 +655,17 @@ abstract class ORMTable implements IORMTable { * @return boolean Success indicator */ public function update( array $values, array $conditions = array() ) { - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->getWriteDbConnection(); - return $dbw->update( + $result = $dbw->update( $this->getName(), $this->getPrefixedValues( $values ), $this->getPrefixedValues( $conditions ), __METHOD__ ) !== false; // DatabaseBase::update does not always return true for success as documented... + + $this->releaseConnection( $dbw ); + return $result; } /** @@ -450,6 +677,7 @@ abstract class ORMTable implements IORMTable { * @param array $conditions */ public function updateSummaryFields( $summaryFields = null, array $conditions = array() ) { + $slave = $this->getReadDb(); $this->setReadDb( DB_MASTER ); /** @@ -461,7 +689,7 @@ abstract class ORMTable implements IORMTable { $item->save(); } - $this->setReadDb( DB_SLAVE ); + $this->setReadDb( $slave ); } /** @@ -559,6 +787,7 @@ abstract class ORMTable implements IORMTable { * Get an instance of this class. * * @since 1.20 + * @deprecated since 1.21 * * @return IORMTable */ @@ -585,10 +814,59 @@ abstract class ORMTable implements IORMTable { */ public function getFieldsFromDBResult( stdClass $result ) { $result = (array)$result; - return array_combine( + + $rawFields = array_combine( $this->unprefixFieldNames( array_keys( $result ) ), array_values( $result ) ); + + $fieldDefinitions = $this->getFields(); + $fields = array(); + + foreach ( $rawFields as $name => $value ) { + if ( array_key_exists( $name, $fieldDefinitions ) ) { + switch ( $fieldDefinitions[$name] ) { + case 'int': + $value = (int)$value; + break; + case 'float': + $value = (float)$value; + break; + case 'bool': + if ( is_string( $value ) ) { + $value = $value !== '0'; + } elseif ( is_int( $value ) ) { + $value = $value !== 0; + } + break; + case 'array': + if ( is_string( $value ) ) { + $value = unserialize( $value ); + } + + if ( !is_array( $value ) ) { + $value = array(); + } + break; + case 'blob': + if ( is_string( $value ) ) { + $value = unserialize( $value ); + } + break; + case 'id': + if ( is_string( $value ) ) { + $value = (int)$value; + } + break; + } + + $fields[$name] = $value; + } else { + throw new MWException( 'Attempted to set unknown field ' . $name ); + } + } + + return $fields; } /** @@ -638,14 +916,15 @@ abstract class ORMTable implements IORMTable { * * @since 1.20 * - * @param array $data + * @param array $fields * @param boolean $loadDefaults * * @return IORMRow */ - public function newRow( array $data, $loadDefaults = false ) { + public function newRow( array $fields, $loadDefaults = false ) { $class = $this->getRowClass(); - return new $class( $this, $data, $loadDefaults ); + + return new $class( $this, $fields, $loadDefaults ); } /** @@ -672,4 +951,157 @@ abstract class ORMTable implements IORMTable { return array_key_exists( $name, $this->getFields() ); } + /** + * Updates the provided row in the database. + * + * @since 1.22 + * + * @param IORMRow $row The row to save + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function updateRow( IORMRow $row, $functionName = null ) { + $dbw = $this->getWriteDbConnection(); + + $success = $dbw->update( + $this->getName(), + $this->getWriteValues( $row ), + $this->getPrefixedValues( array( 'id' => $row->getId() ) ), + is_null( $functionName ) ? __METHOD__ : $functionName + ); + + $this->releaseConnection( $dbw ); + + // DatabaseBase::update does not always return true for success as documented... + return $success !== false; + } + + /** + * Inserts the provided row into the database. + * + * @since 1.22 + * + * @param IORMRow $row + * @param string|null $functionName + * @param array|null $options + * + * @return boolean Success indicator + */ + public function insertRow( IORMRow $row, $functionName = null, array $options = null ) { + $dbw = $this->getWriteDbConnection(); + + $success = $dbw->insert( + $this->getName(), + $this->getWriteValues( $row ), + is_null( $functionName ) ? __METHOD__ : $functionName, + $options + ); + + // DatabaseBase::insert does not always return true for success as documented... + $success = $success !== false; + + if ( $success ) { + $row->setField( 'id', $dbw->insertId() ); + } + + $this->releaseConnection( $dbw ); + + return $success; + } + + /** + * Gets the fields => values to write to the table. + * + * @since 1.22 + * + * @param IORMRow $row + * + * @return array + */ + protected function getWriteValues( IORMRow $row ) { + $values = array(); + + $rowFields = $row->getFields(); + + foreach ( $this->getFields() as $name => $type ) { + if ( array_key_exists( $name, $rowFields ) ) { + $value = $rowFields[$name]; + + switch ( $type ) { + case 'array': + $value = (array)$value; + // fall-through! + case 'blob': + $value = serialize( $value ); + // fall-through! + } + + $values[$this->getPrefixedField( $name )] = $value; + } + } + + return $values; + } + + /** + * Removes the provided row from the database. + * + * @since 1.22 + * + * @param IORMRow $row + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function removeRow( IORMRow $row, $functionName = null ) { + $success = $this->delete( + array( 'id' => $row->getId() ), + is_null( $functionName ) ? __METHOD__ : $functionName + ); + + // DatabaseBase::delete does not always return true for success as documented... + return $success !== false; + } + + /** + * Add an amount (can be negative) to the specified field (needs to be numeric). + * + * @since 1.22 + * + * @param array $conditions + * @param string $field + * @param integer $amount + * + * @return boolean Success indicator + * @throws MWException + */ + public function addToField( array $conditions, $field, $amount ) { + if ( !array_key_exists( $field, $this->fields ) ) { + throw new MWException( 'Unknown field "' . $field . '" provided' ); + } + + if ( $amount == 0 ) { + return true; + } + + $absoluteAmount = abs( $amount ); + $isNegative = $amount < 0; + + $fullField = $this->getPrefixedField( $field ); + + $dbw = $this->getWriteDbConnection(); + + $success = $dbw->update( + $this->getName(), + array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ), + $this->getPrefixedValues( $conditions ), + __METHOD__ + ) !== false; // DatabaseBase::update does not always return true for success as documented... + + $this->releaseConnection( $dbw ); + + return $success; + } + } |