Index: trunk/phase3/maintenance/rebuildrecentchanges.php |
— | — | @@ -66,6 +66,7 @@ |
67 | 67 | $cutoff = time() - $wgRCMaxAge; |
68 | 68 | $dbw->insertSelect( 'recentchanges', array( 'page', 'revision' ), |
69 | 69 | array( |
| 70 | + 'rc_id' => ( $dbw->nextSequenceValue( 'rc_rc_id_seq' ) > 0 ) ? $dbw->nextSequenceValue( 'rc_rc_id_seq' ) : 0, |
70 | 71 | 'rc_timestamp' => 'rev_timestamp', |
71 | 72 | 'rc_cur_time' => 'rev_timestamp', |
72 | 73 | 'rc_user' => 'rev_user', |
Index: trunk/phase3/maintenance/mssql/tables.sql |
— | — | @@ -0,0 +1,764 @@ |
| 2 | +-- Experimental table definitions for Microsoft SQL Server with |
| 3 | +-- content-holding fields switched to explicit BINARY charset. |
| 4 | +-- ------------------------------------------------------------ |
| 5 | + |
| 6 | +-- SQL to create the initial tables for the MediaWiki database. |
| 7 | +-- This is read and executed by the install script; you should |
| 8 | +-- not have to run it by itself unless doing a manual install. |
| 9 | + |
| 10 | +-- |
| 11 | +-- General notes: |
| 12 | +-- |
| 13 | +-- The comments in this and other files are |
| 14 | +-- replaced with the defined table prefix by the installer |
| 15 | +-- and updater scripts. If you are installing or running |
| 16 | +-- updates manually, you will need to manually insert the |
| 17 | +-- table prefix if any when running these scripts. |
| 18 | +-- |
| 19 | + |
| 20 | + |
| 21 | +-- |
| 22 | +-- The user table contains basic account information, |
| 23 | +-- authentication keys, etc. |
| 24 | +-- |
| 25 | +-- Some multi-wiki sites may share a single central user table |
| 26 | +-- between separate wikis using the $wgSharedDB setting. |
| 27 | +-- |
| 28 | +-- Note that when a external authentication plugin is used, |
| 29 | +-- user table entries still need to be created to store |
| 30 | +-- preferences and to key tracking information in the other |
| 31 | +-- tables. |
| 32 | + |
| 33 | +-- LINE:53 |
| 34 | +CREATE TABLE /*$wgDBprefix*/user ( |
| 35 | + user_id INT NOT NULL PRIMARY KEY IDENTITY(0,1), |
| 36 | + user_name NVARCHAR(255) NOT NULL UNIQUE DEFAULT '', |
| 37 | + user_real_name NVARCHAR(255) NOT NULL DEFAULT '', |
| 38 | + user_password NVARCHAR(255) NOT NULL DEFAULT '', |
| 39 | + user_newpassword NVARCHAR(255) NOT NULL DEFAULT '', |
| 40 | + user_newpass_time DATETIME NULL, |
| 41 | + user_email NVARCHAR(255) NOT NULL DEFAULT '', |
| 42 | + user_options NVARCHAR(MAX) NOT NULL DEFAULT '', |
| 43 | + user_touched DATETIME NOT NULL DEFAULT GETDATE(), |
| 44 | + user_token NCHAR(32) NOT NULL DEFAULT '', |
| 45 | + user_email_authenticated DATETIME DEFAULT NULL, |
| 46 | + user_email_token NCHAR(32) DEFAULT '', |
| 47 | + user_email_token_expires DATETIME DEFAULT NULL, |
| 48 | + user_registration DATETIME DEFAULT NULL, |
| 49 | + user_editcount INT NULL |
| 50 | +); |
| 51 | +CREATE INDEX /*$wgDBprefix*/user_email_token ON /*$wgDBprefix*/[user](user_email_token); |
| 52 | +CREATE UNIQUE INDEX /*$wgDBprefix*/[user_name] ON /*$wgDBprefix*/[user]([user_name]); |
| 53 | +; |
| 54 | + |
| 55 | +-- |
| 56 | +-- User permissions have been broken out to a separate table; |
| 57 | +-- this allows sites with a shared user table to have different |
| 58 | +-- permissions assigned to a user in each project. |
| 59 | +-- |
| 60 | +-- This table replaces the old user_rights field which used a |
| 61 | +-- comma-separated blob. |
| 62 | +CREATE TABLE /*$wgDBprefix*/user_groups ( |
| 63 | + ug_user INT NOT NULL REFERENCES /*$wgDBprefix*/[user](user_id) ON DELETE CASCADE, |
| 64 | + ug_group NVARCHAR(16) NOT NULL DEFAULT '', |
| 65 | +); |
| 66 | +CREATE UNIQUE clustered INDEX /*$wgDBprefix*/user_groups_unique ON /*$wgDBprefix*/user_groups(ug_user, ug_group); |
| 67 | +CREATE INDEX /*$wgDBprefix*/user_group ON /*$wgDBprefix*/user_groups(ug_group); |
| 68 | + |
| 69 | +-- Stores notifications of user talk page changes, for the display |
| 70 | +-- of the "you have new messages" box |
| 71 | +-- Changed user_id column to mwuser_id to avoid clashing with user_id function |
| 72 | +CREATE TABLE /*$wgDBprefix*/user_newtalk ( |
| 73 | + user_id INT NOT NULL DEFAULT 0 REFERENCES /*$wgDBprefix*/[user](user_id) ON DELETE CASCADE, |
| 74 | + user_ip NVARCHAR(40) NOT NULL DEFAULT '', |
| 75 | + user_last_timestamp DATETIME NOT NULL DEFAULT '', |
| 76 | +); |
| 77 | +CREATE INDEX /*$wgDBprefix*/user_group_id ON /*$wgDBprefix*/user_newtalk([user_id]); |
| 78 | +CREATE INDEX /*$wgDBprefix*/user_ip ON /*$wgDBprefix*/user_newtalk(user_ip); |
| 79 | + |
| 80 | +-- |
| 81 | +-- User preferences and other fun stuff |
| 82 | +-- replaces old user.user_options BLOB |
| 83 | +-- |
| 84 | +CREATE TABLE /*$wgDBprefix*/user_properties ( |
| 85 | + up_user INT NOT NULL, |
| 86 | + up_property NVARCHAR(32) NOT NULL, |
| 87 | + up_value NVARCHAR(MAX), |
| 88 | +); |
| 89 | +CREATE UNIQUE clustered INDEX /*$wgDBprefix*/user_props_user_prop ON /*$wgDBprefix*/user_properties(up_user, up_property); |
| 90 | +CREATE INDEX /*$wgDBprefix*/user_props_prop ON /*$wgDBprefix*/user_properties(up_property); |
| 91 | + |
| 92 | + |
| 93 | +-- |
| 94 | +-- Core of the wiki: each page has an entry here which identifies |
| 95 | +-- it by title and contains some essential metadata. |
| 96 | +-- |
| 97 | +CREATE TABLE /*$wgDBprefix*/page ( |
| 98 | + page_id INT NOT NULL PRIMARY KEY clustered IDENTITY, |
| 99 | + page_namespace INT NOT NULL, |
| 100 | + page_title NVARCHAR(255) NOT NULL, |
| 101 | + page_restrictions NVARCHAR(255) NULL, |
| 102 | + page_counter BIGINT NOT NULL DEFAULT 0, |
| 103 | + page_is_redirect BIT NOT NULL DEFAULT 0, |
| 104 | + page_is_new BIT NOT NULL DEFAULT 0, |
| 105 | + page_random NUMERIC(15,14) NOT NULL DEFAULT RAND(), |
| 106 | + page_touched DATETIME NOT NULL DEFAULT GETDATE(), |
| 107 | + page_latest INT NOT NULL, |
| 108 | + page_len INT NOT NULL, |
| 109 | +); |
| 110 | +CREATE UNIQUE INDEX /*$wgDBprefix*/page_unique_name ON /*$wgDBprefix*/page(page_namespace, page_title); |
| 111 | +CREATE INDEX /*$wgDBprefix*/page_random_idx ON /*$wgDBprefix*/page(page_random); |
| 112 | +CREATE INDEX /*$wgDBprefix*/page_len_idx ON /*$wgDBprefix*/page(page_len); |
| 113 | +; |
| 114 | + |
| 115 | +-- |
| 116 | +-- Every edit of a page creates also a revision row. |
| 117 | +-- This stores metadata about the revision, and a reference |
| 118 | +-- to the TEXT storage backend. |
| 119 | +-- |
| 120 | +CREATE TABLE /*$wgDBprefix*/revision ( |
| 121 | + rev_id INT NOT NULL UNIQUE IDENTITY, |
| 122 | + rev_page INT NOT NULL, |
| 123 | + rev_text_id INT NOT NULL, |
| 124 | + rev_comment NVARCHAR(max) NOT NULL, |
| 125 | + rev_user INT NOT NULL DEFAULT 0 /*REFERENCES [user](user_id)*/, |
| 126 | + rev_user_text NVARCHAR(255) NOT NULL DEFAULT '', |
| 127 | + rev_timestamp DATETIME NOT NULL DEFAULT GETDATE(), |
| 128 | + rev_minor_edit BIT NOT NULL DEFAULT 0, |
| 129 | + rev_deleted BIT NOT NULL DEFAULT 0, |
| 130 | + rev_len INT, |
| 131 | + rev_parent_id INT DEFAULT NULL, |
| 132 | + |
| 133 | +); |
| 134 | +CREATE UNIQUE clustered INDEX /*$wgDBprefix*/revision_unique ON /*$wgDBprefix*/revision(rev_page, rev_id); |
| 135 | +CREATE UNIQUE INDEX /*$wgDBprefix*/rev_id ON /*$wgDBprefix*/revision(rev_id); |
| 136 | +CREATE INDEX /*$wgDBprefix*/rev_timestamp ON /*$wgDBprefix*/revision(rev_timestamp); |
| 137 | +CREATE INDEX /*$wgDBprefix*/page_timestamp ON /*$wgDBprefix*/revision(rev_page, rev_timestamp); |
| 138 | +CREATE INDEX /*$wgDBprefix*/user_timestamp ON /*$wgDBprefix*/revision(rev_user, rev_timestamp); |
| 139 | +CREATE INDEX /*$wgDBprefix*/usertext_timestamp ON /*$wgDBprefix*/revision(rev_user_text, rev_timestamp); |
| 140 | +; |
| 141 | + |
| 142 | +-- |
| 143 | +-- Holds TEXT of individual page revisions. |
| 144 | +-- |
| 145 | +-- Field names are a holdover from the 'old' revisions table in |
| 146 | +-- MediaWiki 1.4 and earlier: an upgrade will transform that |
| 147 | +-- table INTo the 'text' table to minimize unnecessary churning |
| 148 | +-- and downtime. If upgrading, the other fields will be left unused. |
| 149 | +CREATE TABLE /*$wgDBprefix*/text ( |
| 150 | + old_id INT NOT NULL PRIMARY KEY clustered IDENTITY, |
| 151 | + old_text TEXT NOT NULL, |
| 152 | + old_flags NVARCHAR(255) NOT NULL, |
| 153 | +); |
| 154 | + |
| 155 | +-- |
| 156 | +-- Holding area for deleted articles, which may be viewed |
| 157 | +-- or restored by admins through the Special:Undelete interface. |
| 158 | +-- The fields generally correspond to the page, revision, and text |
| 159 | +-- fields, with several caveats. |
| 160 | +-- Cannot reasonably create views on this table, due to the presence of TEXT |
| 161 | +-- columns. |
| 162 | +CREATE TABLE /*$wgDBprefix*/archive ( |
| 163 | + ar_namespace SMALLINT NOT NULL DEFAULT 0, |
| 164 | + ar_title NVARCHAR(255) NOT NULL DEFAULT '', |
| 165 | + ar_text NVARCHAR(MAX) NOT NULL, |
| 166 | + ar_comment NVARCHAR(255) NOT NULL, |
| 167 | + ar_user INT NULL REFERENCES /*$wgDBprefix*/[user](user_id) ON DELETE SET NULL, |
| 168 | + ar_user_text NVARCHAR(255) NOT NULL, |
| 169 | + ar_timestamp DATETIME NOT NULL DEFAULT GETDATE(), |
| 170 | + ar_minor_edit BIT NOT NULL DEFAULT 0, |
| 171 | + ar_flags NVARCHAR(255) NOT NULL, |
| 172 | + ar_rev_id INT, |
| 173 | + ar_text_id INT, |
| 174 | + ar_deleted BIT NOT NULL DEFAULT 0, |
| 175 | + ar_len INT DEFAULT NULL, |
| 176 | + ar_page_id INT NULL, |
| 177 | + ar_parent_id INT NULL, |
| 178 | +); |
| 179 | +CREATE INDEX /*$wgDBprefix*/ar_name_title_timestamp ON /*$wgDBprefix*/archive(ar_namespace,ar_title,ar_timestamp); |
| 180 | +CREATE INDEX /*$wgDBprefix*/ar_usertext_timestamp ON /*$wgDBprefix*/archive(ar_user_text,ar_timestamp); |
| 181 | +CREATE INDEX /*$wgDBprefix*/ar_user_text ON /*$wgDBprefix*/archive(ar_user_text); |
| 182 | + |
| 183 | + |
| 184 | +-- |
| 185 | +-- Track page-to-page hyperlinks within the wiki. |
| 186 | +-- |
| 187 | +CREATE TABLE /*$wgDBprefix*/pagelinks ( |
| 188 | + pl_from INT NOT NULL DEFAULT 0 REFERENCES /*$wgDBprefix*/page(page_id) ON DELETE CASCADE, |
| 189 | + pl_namespace SMALLINT NOT NULL DEFAULT 0, |
| 190 | + pl_title NVARCHAR(255) NOT NULL DEFAULT '', |
| 191 | +); |
| 192 | +CREATE UNIQUE INDEX /*$wgDBprefix*/pl_from ON /*$wgDBprefix*/pagelinks(pl_from,pl_namespace,pl_title); |
| 193 | +CREATE UNIQUE INDEX /*$wgDBprefix*/pl_namespace ON /*$wgDBprefix*/pagelinks(pl_namespace,pl_title,pl_from); |
| 194 | + |
| 195 | +-- |
| 196 | +-- Track template inclusions. |
| 197 | +-- |
| 198 | +CREATE TABLE /*$wgDBprefix*/templatelinks ( |
| 199 | + tl_from INT NOT NULL DEFAULT 0 REFERENCES /*$wgDBprefix*/page(page_id) ON DELETE CASCADE, |
| 200 | + tl_namespace SMALLINT NOT NULL DEFAULT 0, |
| 201 | + tl_title NVARCHAR(255) NOT NULL DEFAULT '', |
| 202 | +); |
| 203 | +CREATE UNIQUE INDEX /*$wgDBprefix*/tl_from ON /*$wgDBprefix*/templatelinks(tl_from,tl_namespace,tl_title); |
| 204 | +CREATE UNIQUE INDEX /*$wgDBprefix*/tl_namespace ON /*$wgDBprefix*/templatelinks(tl_namespace,tl_title,tl_from); |
| 205 | + |
| 206 | +-- |
| 207 | +-- Track links to images *used inline* |
| 208 | +-- We don't distinguish live from broken links here, so |
| 209 | +-- they do not need to be changed ON upload/removal. |
| 210 | +-- |
| 211 | +CREATE TABLE /*$wgDBprefix*/imagelinks ( |
| 212 | + il_from INT NOT NULL DEFAULT 0 REFERENCES /*$wgDBprefix*/page(page_id) ON DELETE CASCADE, |
| 213 | + il_to NVARCHAR(255) NOT NULL DEFAULT '', |
| 214 | + CONSTRAINT /*$wgDBprefix*/il_from PRIMARY KEY(il_from,il_to), |
| 215 | +); |
| 216 | +CREATE UNIQUE INDEX /*$wgDBprefix*/il_from_to ON /*$wgDBprefix*/imagelinks(il_from,il_to); |
| 217 | +CREATE UNIQUE INDEX /*$wgDBprefix*/il_to_from ON /*$wgDBprefix*/imagelinks(il_to,il_from); |
| 218 | + |
| 219 | +-- |
| 220 | +-- Track category inclusions *used inline* |
| 221 | +-- This tracks a single level of category membership |
| 222 | +-- (folksonomic tagging, really). |
| 223 | +-- |
| 224 | +CREATE TABLE /*$wgDBprefix*/categorylinks ( |
| 225 | + cl_from INT NOT NULL DEFAULT 0, |
| 226 | + cl_to NVARCHAR(255) NOT NULL DEFAULT '', |
| 227 | + cl_sortkey NVARCHAR(150) NOT NULL DEFAULT '', |
| 228 | + cl_timestamp DATETIME NOT NULL DEFAULT GETDATE(), |
| 229 | + CONSTRAINT /*$wgDBprefix*/cl_from PRIMARY KEY(cl_from, cl_to), |
| 230 | +); |
| 231 | +CREATE UNIQUE INDEX /*$wgDBprefix*/cl_from_to ON /*$wgDBprefix*/categorylinks(cl_from,cl_to); |
| 232 | +-- We always sort within a given category... |
| 233 | +CREATE INDEX /*$wgDBprefix*/cl_sortkey ON /*$wgDBprefix*/categorylinks(cl_to,cl_sortkey); |
| 234 | +-- Not really used? |
| 235 | +CREATE INDEX /*$wgDBprefix*/cl_timestamp ON /*$wgDBprefix*/categorylinks(cl_to,cl_timestamp); |
| 236 | +--; |
| 237 | + |
| 238 | +-- |
| 239 | +-- Track all existing categories. Something is a category if 1) it has an en- |
| 240 | +-- try somewhere in categorylinks, or 2) it once did. Categories might not |
| 241 | +-- have corresponding pages, so they need to be tracked separately. |
| 242 | +-- |
| 243 | +CREATE TABLE /*$wgDBprefix*/category ( |
| 244 | + cat_id int NOT NULL IDENTITY(1,1), |
| 245 | + cat_title nvarchar(255) NOT NULL, |
| 246 | + cat_pages int NOT NULL default 0, |
| 247 | + cat_subcats int NOT NULL default 0, |
| 248 | + cat_files int NOT NULL default 0, |
| 249 | + cat_hidden tinyint NOT NULL default 0, |
| 250 | +); |
| 251 | + |
| 252 | +CREATE UNIQUE INDEX /*$wgDBprefix*/cat_title ON /*$wgDBprefix*/category(cat_title); |
| 253 | +-- For Special:Mostlinkedcategories |
| 254 | +CREATE INDEX /*$wgDBprefix*/cat_pages ON /*$wgDBprefix*/category(cat_pages); |
| 255 | + |
| 256 | + |
| 257 | +CREATE TABLE /*$wgDBprefix*/change_tag ( |
| 258 | + ct_rc_id int NOT NULL default 0, |
| 259 | + ct_log_id int NOT NULL default 0, |
| 260 | + ct_rev_id int NOT NULL default 0, |
| 261 | + ct_tag varchar(255) NOT NULL, |
| 262 | + ct_params varchar(255) NOT NULL, |
| 263 | +); |
| 264 | +CREATE UNIQUE INDEX /*$wgDBprefix*/change_tag_rc_tag ON /*$wgDBprefix*/change_tag(ct_rc_id,ct_tag); |
| 265 | +CREATE UNIQUE INDEX /*$wgDBprefix*/change_tag_log_tag ON /*$wgDBprefix*/change_tag(ct_log_id,ct_tag); |
| 266 | +CREATE UNIQUE INDEX /*$wgDBprefix*/change_tag_rev_tag ON /*$wgDBprefix*/change_tag(ct_rev_id,ct_tag); |
| 267 | +CREATE INDEX /*$wgDBprefix*/change_tag_tag_id ON /*$wgDBprefix*/change_tag(ct_tag,ct_rc_id,ct_rev_id,ct_log_id); |
| 268 | + |
| 269 | +CREATE TABLE /*$wgDBprefix*/tag_summary ( |
| 270 | + ts_rc_id INT NOT NULL default 0, |
| 271 | + ts_log_id INT NOT NULL default 0, |
| 272 | + ts_rev_id INT NOT NULL default 0, |
| 273 | + ts_tags varchar(255) NOT NULL |
| 274 | +); |
| 275 | +CREATE UNIQUE INDEX /*$wgDBprefix*/tag_summary_rc_id ON /*$wgDBprefix*/tag_summary(ts_rc_id); |
| 276 | +CREATE UNIQUE INDEX /*$wgDBprefix*/tag_summary_log_id ON /*$wgDBprefix*/tag_summary(ts_log_id); |
| 277 | +CREATE UNIQUE INDEX /*$wgDBprefix*/tag_summary_rev_id ON /*$wgDBprefix*/tag_summary(ts_rev_id); |
| 278 | + |
| 279 | +CREATE TABLE /*$wgDBprefix*/valid_tag ( |
| 280 | + vt_tag varchar(255) NOT NULL PRIMARY KEY |
| 281 | +); |
| 282 | + |
| 283 | +-- |
| 284 | +-- Table for storing localisation data |
| 285 | +-- |
| 286 | +CREATE TABLE /*$wgDBprefix*/l10n_cache ( |
| 287 | + -- language code |
| 288 | + lc_lang NVARCHAR(32) NOT NULL, |
| 289 | + |
| 290 | + -- cache key |
| 291 | + lc_key NVARCHAR(255) NOT NULL, |
| 292 | + |
| 293 | + -- Value |
| 294 | + lc_value TEXT NOT NULL DEFAULT '', |
| 295 | +); |
| 296 | +CREATE INDEX /*$wgDBprefix*/lc_lang_key ON /*$wgDBprefix*/l10n_cache (lc_lang, lc_key); |
| 297 | + |
| 298 | +-- |
| 299 | +-- Track links to external URLs |
| 300 | +-- IE >= 4 supports no more than 2083 characters in a URL |
| 301 | +CREATE TABLE /*$wgDBprefix*/externallinks ( |
| 302 | + el_from INT NOT NULL DEFAULT '0', |
| 303 | + el_to VARCHAR(2083) NOT NULL, |
| 304 | + el_index VARCHAR(896) NOT NULL, |
| 305 | +); |
| 306 | +-- Maximum key length ON SQL Server is 900 bytes |
| 307 | +CREATE INDEX /*$wgDBprefix*/externallinks_index ON /*$wgDBprefix*/externallinks(el_index); |
| 308 | + |
| 309 | +-- |
| 310 | +-- Track external user accounts, if ExternalAuth is used |
| 311 | +-- |
| 312 | +CREATE TABLE /*$wgDBprefix*/external_user ( |
| 313 | + -- Foreign key to user_id |
| 314 | + eu_local_id INT NOT NULL PRIMARY KEY, |
| 315 | + -- opaque identifier provided by the external database |
| 316 | + eu_external_id NVARCHAR(255) NOT NULL, |
| 317 | +); |
| 318 | +CREATE UNIQUE INDEX /*$wgDBprefix*/eu_external_idx ON /*$wgDBprefix*/external_user(eu_external_id); |
| 319 | + |
| 320 | +-- |
| 321 | +-- Track INTerlanguage links |
| 322 | +-- |
| 323 | +CREATE TABLE /*$wgDBprefix*/langlinks ( |
| 324 | + ll_from INT NOT NULL DEFAULT 0, |
| 325 | + ll_lang NVARCHAR(20) NOT NULL DEFAULT '', |
| 326 | + ll_title NVARCHAR(255) NOT NULL DEFAULT '', |
| 327 | + CONSTRAINT /*$wgDBprefix*/langlinks_pk PRIMARY KEY(ll_from, ll_lang), |
| 328 | +); |
| 329 | +CREATE UNIQUE INDEX /*$wgDBprefix*/langlinks_reverse_key ON /*$wgDBprefix*/langlinks(ll_lang,ll_title); |
| 330 | + |
| 331 | +-- |
| 332 | +-- Track inline interwiki links |
| 333 | +-- |
| 334 | +CREATE TABLE /*$wgDBprefix*/iwlinks ( |
| 335 | + -- page_id of the referring page |
| 336 | + iwl_from INT NOT NULL DEFAULT 0, |
| 337 | + |
| 338 | + -- Interwiki prefix code of the target |
| 339 | + iwl_prefix NVARCHAR(20) NOT NULL DEFAULT '', |
| 340 | + |
| 341 | + -- Title of the target, including namespace |
| 342 | + iwl_title NVARCHAR(255) NOT NULL DEFAULT '', |
| 343 | +); |
| 344 | + |
| 345 | +CREATE UNIQUE INDEX /*$wgDBprefix*/iwl_from ON /*$wgDBprefix*/iwlinks(iwl_from,iwl_prefix,iwl_title); |
| 346 | +CREATE UNIQUE INDEX /*$wgDBprefix*/iwl_prefix ON /*$wgDBprefix*/iwlinks(iwl_prefix,iwl_title); |
| 347 | + |
| 348 | + |
| 349 | +-- |
| 350 | +-- Contains a single row with some aggregate info |
| 351 | +-- ON the state of the site. |
| 352 | +-- |
| 353 | +CREATE TABLE /*$wgDBprefix*/site_stats ( |
| 354 | + ss_row_id INT NOT NULL DEFAULT 1 PRIMARY KEY, |
| 355 | + ss_total_views BIGINT DEFAULT 0, |
| 356 | + ss_total_edits BIGINT DEFAULT 0, |
| 357 | + ss_good_articles BIGINT DEFAULT 0, |
| 358 | + ss_total_pages BIGINT DEFAULT -1, |
| 359 | + ss_users BIGINT DEFAULT -1, |
| 360 | + ss_active_users BIGINT DEFAULT -1, |
| 361 | + ss_admins INT DEFAULT -1, |
| 362 | + ss_images INT DEFAULT 0, |
| 363 | +); |
| 364 | + |
| 365 | +-- INSERT INTO site_stats DEFAULT VALUES; |
| 366 | + |
| 367 | +-- |
| 368 | +-- Stores an ID for every time any article is visited; |
| 369 | +-- depending ON $wgHitcounterUpdateFreq, it is |
| 370 | +-- periodically cleared and the page_counter column |
| 371 | +-- in the page table updated for the all articles |
| 372 | +-- that have been visited.) |
| 373 | +-- |
| 374 | +CREATE TABLE /*$wgDBprefix*/hitcounter ( |
| 375 | + hc_id BIGINT NOT NULL |
| 376 | +); |
| 377 | + |
| 378 | +-- |
| 379 | +-- The Internet is full of jerks, alas. Sometimes it's handy |
| 380 | +-- to block a vandal or troll account. |
| 381 | +-- |
| 382 | +CREATE TABLE /*$wgDBprefix*/ipblocks ( |
| 383 | + ipb_id INT NOT NULL PRIMARY KEY, |
| 384 | + ipb_address NVARCHAR(255) NOT NULL, |
| 385 | + ipb_user INT NOT NULL DEFAULT 0, |
| 386 | + ipb_by INT NOT NULL DEFAULT 0, |
| 387 | + ipb_by_text NVARCHAR(255) NOT NULL DEFAULT '', |
| 388 | + ipb_reason NVARCHAR(255) NOT NULL, |
| 389 | + ipb_timestamp DATETIME NOT NULL DEFAULT GETDATE(), |
| 390 | + ipb_auto BIT NOT NULL DEFAULT 0, |
| 391 | + ipb_anon_only BIT NOT NULL DEFAULT 0, |
| 392 | + ipb_create_account BIT NOT NULL DEFAULT 1, |
| 393 | + ipb_enable_autoblock BIT NOT NULL DEFAULT 1, |
| 394 | + ipb_expiry DATETIME NOT NULL DEFAULT GETDATE(), |
| 395 | + ipb_range_start NVARCHAR(32) NOT NULL DEFAULT '', |
| 396 | + ipb_range_end NVARCHAR(32) NOT NULL DEFAULT '', |
| 397 | + ipb_deleted BIT NOT NULL DEFAULT 0, |
| 398 | + ipb_block_email BIT NOT NULL DEFAULT 0, |
| 399 | + ipb_allow_usertalk BIT NOT NULL DEFAULT 0, |
| 400 | +); |
| 401 | +-- Unique index to support "user already blocked" messages |
| 402 | +-- Any new options which prevent collisions should be included |
| 403 | +--UNIQUE INDEX ipb_address (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only), |
| 404 | +CREATE UNIQUE INDEX /*$wgDBprefix*/ipb_address ON /*$wgDBprefix*/ipblocks(ipb_address, ipb_user, ipb_auto, ipb_anon_only); |
| 405 | +CREATE INDEX /*$wgDBprefix*/ipb_user ON /*$wgDBprefix*/ipblocks(ipb_user); |
| 406 | +CREATE INDEX /*$wgDBprefix*/ipb_range ON /*$wgDBprefix*/ipblocks(ipb_range_start, ipb_range_end); |
| 407 | +CREATE INDEX /*$wgDBprefix*/ipb_timestamp ON /*$wgDBprefix*/ipblocks(ipb_timestamp); |
| 408 | +CREATE INDEX /*$wgDBprefix*/ipb_expiry ON /*$wgDBprefix*/ipblocks(ipb_expiry); |
| 409 | +; |
| 410 | + |
| 411 | +-- |
| 412 | +-- Uploaded images and other files. |
| 413 | +CREATE TABLE /*$wgDBprefix*/image ( |
| 414 | + img_name varchar(255) NOT NULL default '', |
| 415 | + img_size INT NOT NULL DEFAULT 0, |
| 416 | + img_width INT NOT NULL DEFAULT 0, |
| 417 | + img_height INT NOT NULL DEFAULT 0, |
| 418 | + img_metadata TEXT NOT NULL, -- was MEDIUMBLOB |
| 419 | + img_bits SMALLINT NOT NULL DEFAULT 0, |
| 420 | + img_media_type NVARCHAR(MAX) DEFAULT 'UNKNOWN', |
| 421 | + img_major_mime NVARCHAR(MAX) DEFAULT 'UNKNOWN', |
| 422 | + img_minor_mime NVARCHAR(MAX) NOT NULL DEFAULT 'unknown', |
| 423 | + img_description NVARCHAR(MAX) NOT NULL, |
| 424 | + img_user INT NOT NULL DEFAULT 0, |
| 425 | + img_user_text VARCHAR(255) NOT NULL DEFAULT '', |
| 426 | + img_timestamp DATETIME NOT NULL DEFAULT GETDATE(), |
| 427 | + img_sha1 VARCHAR(255) NOT NULL default '', |
| 428 | +); |
| 429 | +-- Used by Special:Imagelist for sort-by-size |
| 430 | +CREATE INDEX /*$wgDBprefix*/img_size ON /*$wgDBprefix*/[image](img_size); |
| 431 | +-- Used by Special:Newimages and Special:Imagelist |
| 432 | +CREATE INDEX /*$wgDBprefix*/img_timestamp ON /*$wgDBprefix*/[image](img_timestamp) |
| 433 | +CREATE INDEX /*$wgDBprefix*/[img_sha1] ON /*wgDBprefix*/[image](img_sha1) |
| 434 | + |
| 435 | +-- |
| 436 | +-- Previous revisions of uploaded files. |
| 437 | +-- Awkwardly, image rows have to be moved into |
| 438 | +-- this table at re-upload time. |
| 439 | +-- |
| 440 | +CREATE TABLE /*$wgDBprefix*/oldimage ( |
| 441 | + oi_name VARCHAR(255) NOT NULL DEFAULT '', |
| 442 | + oi_archive_name VARCHAR(255) NOT NULL DEFAULT '', |
| 443 | + oi_size INT NOT NULL DEFAULT 0, |
| 444 | + oi_width INT NOT NULL DEFAULT 0, |
| 445 | + oi_height INT NOT NULL DEFAULT 0, |
| 446 | + oi_bits SMALLINT NOT NULL DEFAULT 0, |
| 447 | + oi_description NVARCHAR(MAX) NOT NULL, |
| 448 | + oi_user INT NOT NULL DEFAULT 0, |
| 449 | + oi_user_text VARCHAR(255) NOT NULL DEFAULT '', |
| 450 | + oi_timestamp DATETIME NOT NULL DEFAULT GETDATE(), |
| 451 | + oi_metadata TEXT NOT NULL, |
| 452 | + oi_media_type NVARCHAR(MAX) DEFAULT 'UNKNOWN', |
| 453 | + oi_major_mime NVARCHAR(MAX) NOT NULL DEFAULT 'UNKNOWN', |
| 454 | + oi_minor_mime NVARCHAR(MAX) NOT NULL DEFAULT 'unknown', |
| 455 | + oi_deleted BIT NOT NULL default 0, |
| 456 | + oi_sha1 VARCHAR(255) NOT NULL default '', |
| 457 | +); |
| 458 | +CREATE INDEX /*$wgDBprefix*/oi_usertext_timestamp ON /*$wgDBprefix*/oldimage(oi_user_text,oi_timestamp); |
| 459 | +CREATE INDEX /*$wgDBprefix*/oi_name_timestamp ON /*$wgDBprefix*/oldimage(oi_name, oi_timestamp); |
| 460 | +CREATE INDEX /*$wgDBprefix*/oi_name_archive_name ON /*$wgDBprefix*/oldimage(oi_name,oi_archive_name); |
| 461 | +CREATE INDEX /*$wgDBprefix*/[oi_sha1] ON /*$wgDBprefix*/oldimage(oi_sha1); |
| 462 | + |
| 463 | +-- |
| 464 | +-- Record of deleted file data |
| 465 | +-- |
| 466 | +CREATE TABLE /*$wgDBprefix*/filearchive ( |
| 467 | + fa_id INT NOT NULL PRIMARY KEY, |
| 468 | + fa_name NVARCHAR(255) NOT NULL DEFAULT '', |
| 469 | + fa_archive_name NVARCHAR(255) DEFAULT '', |
| 470 | + fa_storage_group NVARCHAR(16), |
| 471 | + fa_storage_key NVARCHAR(64) DEFAULT '', |
| 472 | + fa_deleted_user INT, |
| 473 | + fa_deleted_timestamp NVARCHAR(14) DEFAULT NULL, |
| 474 | + fa_deleted_reason NVARCHAR(255), |
| 475 | + fa_size SMALLINT DEFAULT 0, |
| 476 | + fa_width SMALLINT DEFAULT 0, |
| 477 | + fa_height SMALLINT DEFAULT 0, |
| 478 | + fa_metadata NVARCHAR(MAX), -- was mediumblob |
| 479 | + fa_bits SMALLINT DEFAULT 0, |
| 480 | + fa_media_type NVARCHAR(11) DEFAULT NULL, |
| 481 | + fa_major_mime NVARCHAR(11) DEFAULT 'unknown', |
| 482 | + fa_minor_mime NVARCHAR(32) DEFAULT 'unknown', |
| 483 | + fa_description NVARCHAR(255), |
| 484 | + fa_user INT DEFAULT 0, |
| 485 | + fa_user_text NVARCHAR(255) DEFAULT '', |
| 486 | + fa_timestamp DATETIME DEFAULT GETDATE(), |
| 487 | + fa_deleted BIT NOT NULL DEFAULT 0, |
| 488 | +); |
| 489 | +-- Pick by image name |
| 490 | +CREATE INDEX /*$wgDBprefix*/filearchive_name ON /*$wgDBprefix*/filearchive(fa_name,fa_timestamp); |
| 491 | +-- Pick by dupe files |
| 492 | +CREATE INDEX /*$wgDBprefix*/filearchive_dupe ON /*$wgDBprefix*/filearchive(fa_storage_group,fa_storage_key); |
| 493 | +-- Pick by deletion time |
| 494 | +CREATE INDEX /*$wgDBprefix*/filearchive_time ON /*$wgDBprefix*/filearchive(fa_deleted_timestamp); |
| 495 | +-- Pick by deleter |
| 496 | +CREATE INDEX /*$wgDBprefix*/filearchive_user ON /*$wgDBprefix*/filearchive(fa_deleted_user); |
| 497 | + |
| 498 | +-- |
| 499 | +-- Primarily a summary table for Special:Recentchanges, |
| 500 | +-- this table contains some additional info on edits from |
| 501 | +-- the last few days, see Article::editUpdates() |
| 502 | +-- |
| 503 | +CREATE TABLE /*$wgDBprefix*/recentchanges ( |
| 504 | + rc_id INT NOT NULL, |
| 505 | + rc_timestamp DATETIME DEFAULT GETDATE(), |
| 506 | + rc_cur_time DATETIME DEFAULT GETDATE(), |
| 507 | + rc_user INT DEFAULT 0, |
| 508 | + rc_user_text NVARCHAR(255) DEFAULT '', |
| 509 | + rc_namespace SMALLINT DEFAULT 0, |
| 510 | + rc_title NVARCHAR(255) DEFAULT '', |
| 511 | + rc_comment NVARCHAR(255) DEFAULT '', |
| 512 | + rc_minor BIT DEFAULT 0, |
| 513 | + rc_bot BIT DEFAULT 0, |
| 514 | + rc_new BIT DEFAULT 0, |
| 515 | + rc_cur_id INT DEFAULT 0, |
| 516 | + rc_this_oldid INT DEFAULT 0, |
| 517 | + rc_last_oldid INT DEFAULT 0, |
| 518 | + rc_type tinyint DEFAULT 0, |
| 519 | + rc_moved_to_ns BIT DEFAULT 0, |
| 520 | + rc_moved_to_title NVARCHAR(255) DEFAULT '', |
| 521 | + rc_patrolled BIT DEFAULT 0, |
| 522 | + rc_ip NCHAR(40) DEFAULT '', |
| 523 | + rc_old_len INT DEFAULT 0, |
| 524 | + rc_new_len INT DEFAULT 0, |
| 525 | + rc_deleted BIT DEFAULT 0, |
| 526 | + rc_logid INT DEFAULT 0, |
| 527 | + rc_log_type NVARCHAR(255) NULL DEFAULT NULL, |
| 528 | + rc_log_action NVARCHAR(255) NULL DEFAULT NULL, |
| 529 | + rc_params NVARCHAR(MAX) DEFAULT '', |
| 530 | +); |
| 531 | +CREATE INDEX /*$wgDBprefix*/rc_timestamp ON /*$wgDBprefix*/recentchanges(rc_timestamp); |
| 532 | +CREATE INDEX /*$wgDBprefix*/rc_namespace_title ON /*$wgDBprefix*/recentchanges(rc_namespace, rc_title); |
| 533 | +CREATE INDEX /*$wgDBprefix*/rc_cur_id ON /*$wgDBprefix*/recentchanges(rc_cur_id); |
| 534 | +CREATE INDEX /*$wgDBprefix*/new_name_timestamp ON /*$wgDBprefix*/recentchanges(rc_new,rc_namespace,rc_timestamp); |
| 535 | +CREATE INDEX /*$wgDBprefix*/rc_ip ON /*$wgDBprefix*/recentchanges(rc_ip); |
| 536 | +CREATE INDEX /*$wgDBprefix*/rc_ns_usertext ON /*$wgDBprefix*/recentchanges(rc_namespace, rc_user_text); |
| 537 | +CREATE INDEX /*$wgDBprefix*/rc_user_text ON /*$wgDBprefix*/recentchanges(rc_user_text, rc_timestamp); |
| 538 | +; |
| 539 | + |
| 540 | +CREATE TABLE /*$wgDBprefix*/watchlist ( |
| 541 | + wl_user INT NOT NULL, |
| 542 | + wl_namespace SMALLINT NOT NULL DEFAULT 0, |
| 543 | + wl_title NVARCHAR(255) NOT NULL DEFAULT '', |
| 544 | + wl_notificationtimestamp NVARCHAR(14) DEFAULT NULL, |
| 545 | + |
| 546 | +); |
| 547 | +CREATE UNIQUE INDEX /*$wgDBprefix*/namespace_title ON /*$wgDBprefix*/watchlist(wl_namespace,wl_title); |
| 548 | + |
| 549 | +-- |
| 550 | +-- Used by the math module to keep track |
| 551 | +-- of previously-rendered items. |
| 552 | +-- |
| 553 | +CREATE TABLE /*$wgDBprefix*/math ( |
| 554 | + math_inputhash varbinary(16) NOT NULL PRIMARY KEY, |
| 555 | + math_outputhash varbinary(16) NOT NULL, |
| 556 | + math_html_conservativeness tinyint NOT NULL, |
| 557 | + math_html NVARCHAR(MAX), |
| 558 | + math_mathml NVARCHAR(MAX), |
| 559 | +); |
| 560 | + |
| 561 | +-- Needs fulltext index. |
| 562 | +CREATE TABLE /*$wgDBprefix*/searchindex ( |
| 563 | + si_page INT NOT NULL unique REFERENCES /*$wgDBprefix*/page(page_id) ON DELETE CASCADE, |
| 564 | + si_title varbinary(max) NOT NULL, |
| 565 | + si_text varbinary(max) NOT NULL, |
| 566 | + si_ext CHAR(4) NOT NULL DEFAULT '.txt', |
| 567 | +); |
| 568 | +CREATE FULLTEXT CATALOG wikidb AS DEFAULT; |
| 569 | +CREATE UNIQUE CLUSTERED INDEX searchindex_page ON searchindex (si_page); |
| 570 | +CREATE FULLTEXT INDEX on searchindex (si_title TYPE COLUMN si_ext, si_text TYPE COLUMN si_ext) |
| 571 | +KEY INDEX searchindex_page |
| 572 | +; |
| 573 | + |
| 574 | +-- This table is not used unless profiling is turned on |
| 575 | +CREATE TABLE profiling ( |
| 576 | + pf_count INTEGER NOT NULL DEFAULT 0, |
| 577 | + pf_time NUMERIC(18,10) NOT NULL DEFAULT 0, |
| 578 | + pf_name NVARCHAR(200) NOT NULL, |
| 579 | + pf_server NVARCHAR(200) NULL |
| 580 | +); |
| 581 | +CREATE UNIQUE INDEX pf_name_server ON profiling (pf_name, pf_server); |
| 582 | + |
| 583 | +-- |
| 584 | +-- Recognized INTerwiki link prefixes |
| 585 | +-- |
| 586 | +CREATE TABLE /*$wgDBprefix*/interwiki ( |
| 587 | + iw_prefix NCHAR(32) NOT NULL PRIMARY KEY, |
| 588 | + iw_url NCHAR(127) NOT NULL, |
| 589 | + iw_api TEXT NOT NULL DEFAULT '', |
| 590 | + iw_wikiid NVARCHAR(64) NOT NULL DEFAULT '', |
| 591 | + iw_local BIT NOT NULL, |
| 592 | + iw_trans BIT NOT NULL DEFAULT 0, |
| 593 | +); |
| 594 | + |
| 595 | +-- |
| 596 | +-- Used for caching expensive grouped queries |
| 597 | +-- |
| 598 | +CREATE TABLE /*$wgDBprefix*/querycache ( |
| 599 | + qc_type NCHAR(32) NOT NULL, |
| 600 | + qc_value INT NOT NULL DEFAULT '0', |
| 601 | + qc_namespace SMALLINT NOT NULL DEFAULT 0, |
| 602 | + qc_title NCHAR(255) NOT NULL DEFAULT '', |
| 603 | + CONSTRAINT /*$wgDBprefix*/qc_pk PRIMARY KEY (qc_type,qc_value) |
| 604 | +); |
| 605 | + |
| 606 | +-- |
| 607 | +-- For a few generic cache operations if not using Memcached |
| 608 | +-- |
| 609 | +CREATE TABLE /*$wgDBprefix*/objectcache ( |
| 610 | + keyname NCHAR(255) NOT NULL DEFAULT '', |
| 611 | + [value] NVARCHAR(MAX), -- IMAGE, |
| 612 | + exptime DATETIME, -- This is treated as a DATETIME |
| 613 | +); |
| 614 | +CREATE CLUSTERED INDEX /*$wgDBprefix*/[objectcache_time] ON /*$wgDBprefix*/objectcache(exptime); |
| 615 | +CREATE UNIQUE INDEX /*$wgDBprefix*/[objectcache_PK] ON /*wgDBprefix*/objectcache(keyname); |
| 616 | +-- |
| 617 | +-- Cache of INTerwiki transclusion |
| 618 | +-- |
| 619 | +CREATE TABLE /*$wgDBprefix*/transcache ( |
| 620 | + tc_url NVARCHAR(255) NOT NULL PRIMARY KEY, |
| 621 | + tc_contents NVARCHAR(MAX), |
| 622 | + tc_time INT NOT NULL, |
| 623 | +); |
| 624 | + |
| 625 | +CREATE TABLE /*$wgDBprefix*/logging ( |
| 626 | + log_id INT PRIMARY KEY IDENTITY, |
| 627 | + log_type NCHAR(10) NOT NULL DEFAULT '', |
| 628 | + log_action NCHAR(10) NOT NULL DEFAULT '', |
| 629 | + log_timestamp DATETIME NOT NULL DEFAULT GETDATE(), |
| 630 | + log_user INT NOT NULL DEFAULT 0, |
| 631 | + log_user_text NVARCHAR(255) NOT NULL DEFAULT '', |
| 632 | + log_namespace INT NOT NULL DEFAULT 0, |
| 633 | + log_title NVARCHAR(255) NOT NULL DEFAULT '', |
| 634 | + log_page INT NULL DEFAULT NULL, |
| 635 | + log_comment NVARCHAR(255) NOT NULL DEFAULT '', |
| 636 | + log_params NVARCHAR(MAX) NOT NULL, |
| 637 | + log_deleted BIT NOT NULL DEFAULT 0, |
| 638 | +); |
| 639 | +CREATE INDEX /*$wgDBprefix*/type_time ON /*$wgDBprefix*/logging (log_type, log_timestamp); |
| 640 | +CREATE INDEX /*$wgDBprefix*/user_time ON /*$wgDBprefix*/logging (log_user, log_timestamp); |
| 641 | +CREATE INDEX /*$wgDBprefix*/page_time ON /*$wgDBprefix*/logging (log_namespace, log_title, log_timestamp); |
| 642 | +CREATE INDEX /*$wgDBprefix*/times ON /*$wgDBprefix*/logging (log_timestamp); |
| 643 | +CREATE INDEX /*$wgDBprefix*/log_user_type_time ON /*$wgDBprefix*/logging (log_user, log_type, log_timestamp); |
| 644 | +CREATE INDEX /*$wgDBprefix*/log_page_id_time ON /*$wgDBprefix*/logging (log_page,log_timestamp); |
| 645 | + |
| 646 | +CREATE TABLE /*$wgDBprefix*/log_search ( |
| 647 | + -- The type of ID (rev ID, log ID, rev timestamp, username) |
| 648 | + ls_field NVARCHAR(32) NOT NULL, |
| 649 | + -- The value of the ID |
| 650 | + ls_value NVARCHAR(255) NOT NULL, |
| 651 | + -- Key to log_id |
| 652 | + ls_log_id INT NOT NULL default 0, |
| 653 | +); |
| 654 | +CREATE UNIQUE INDEX /*$wgDBprefix*/ls_field_val ON /*$wgDBprefix*/log_search (ls_field,ls_value,ls_log_id); |
| 655 | +CREATE INDEX /*$wgDBprefix*/ls_log_id ON /*$wgDBprefix*/log_search (ls_log_id); |
| 656 | + |
| 657 | + |
| 658 | +CREATE TABLE /*$wgDBprefix*/trackbacks ( |
| 659 | + tb_id INT PRIMARY KEY, |
| 660 | + tb_page INT REFERENCES /*$wgDBprefix*/page(page_id) ON DELETE CASCADE, |
| 661 | + tb_title NVARCHAR(255) NOT NULL, |
| 662 | + tb_url NVARCHAR(255) NOT NULL, |
| 663 | + tb_ex NVARCHAR(MAX), |
| 664 | + tb_name NVARCHAR(255), |
| 665 | +); |
| 666 | +CREATE INDEX /*$wgDBprefix*/trackbacks_page ON /*$wgDBprefix*/trackbacks(tb_page); |
| 667 | + |
| 668 | +-- Jobs performed by parallel apache threads or a command-line daemon |
| 669 | +CREATE TABLE /*$wgDBprefix*/job ( |
| 670 | + job_id INT NOT NULL PRIMARY KEY, |
| 671 | + job_cmd NVARCHAR(200) NOT NULL DEFAULT '', |
| 672 | + job_namespace INT NOT NULL, |
| 673 | + job_title NVARCHAR(200) NOT NULL, |
| 674 | + job_params NVARCHAR(255) NOT NULL, |
| 675 | +); |
| 676 | +CREATE INDEX /*$wgDBprefix*/job_idx ON /*$wgDBprefix*/job(job_cmd,job_namespace,job_title); |
| 677 | + |
| 678 | +-- Details of updates to cached special pages |
| 679 | +CREATE TABLE /*$wgDBprefix*/querycache_info ( |
| 680 | + qci_type NVARCHAR(32) NOT NULL DEFAULT '' PRIMARY KEY, |
| 681 | + qci_timestamp NVARCHAR(14) NOT NULL DEFAULT '19700101000000', |
| 682 | +); |
| 683 | + |
| 684 | +-- For each redirect, this table contains exactly one row defining its target |
| 685 | +CREATE TABLE /*$wgDBprefix*/redirect ( |
| 686 | + rd_from INT NOT NULL DEFAULT 0 REFERENCES /*$wgDBprefix*/[page](page_id) ON DELETE CASCADE, |
| 687 | + rd_namespace SMALLINT NOT NULL DEFAULT '0', |
| 688 | + rd_title NVARCHAR(255) NOT NULL DEFAULT '', |
| 689 | + rd_interwiki NVARCHAR(32) DEFAULT NULL, |
| 690 | + rd_fragment NVARCHAR(255) DEFAULT NULL, |
| 691 | +); |
| 692 | +CREATE UNIQUE INDEX /*$wgDBprefix*/rd_ns_title ON /*$wgDBprefix*/redirect(rd_namespace,rd_title,rd_from); |
| 693 | + |
| 694 | +-- Used for caching expensive grouped queries that need two links (for example double-redirects) |
| 695 | +CREATE TABLE /*$wgDBprefix*/querycachetwo ( |
| 696 | + qcc_type NCHAR(32) NOT NULL, |
| 697 | + qcc_value INT NOT NULL DEFAULT 0, |
| 698 | + qcc_namespace INT NOT NULL DEFAULT 0, |
| 699 | + qcc_title NCHAR(255) NOT NULL DEFAULT '', |
| 700 | + qcc_namespacetwo INT NOT NULL DEFAULT 0, |
| 701 | + qcc_titletwo NCHAR(255) NOT NULL DEFAULT '', |
| 702 | + CONSTRAINT /*$wgDBprefix*/qcc_type PRIMARY KEY(qcc_type,qcc_value), |
| 703 | +); |
| 704 | +CREATE UNIQUE INDEX /*$wgDBprefix*/qcc_title ON /*$wgDBprefix*/querycachetwo(qcc_type,qcc_namespace,qcc_title); |
| 705 | +CREATE UNIQUE INDEX /*$wgDBprefix*/qcc_titletwo ON /*$wgDBprefix*/querycachetwo(qcc_type,qcc_namespacetwo,qcc_titletwo); |
| 706 | + |
| 707 | + |
| 708 | +--- Used for storing page restrictions (i.e. protection levels) |
| 709 | +CREATE TABLE /*$wgDBprefix*/page_restrictions ( |
| 710 | + pr_page INT NOT NULL REFERENCES /*$wgDBprefix*/page(page_id) ON DELETE CASCADE, |
| 711 | + pr_type NVARCHAR(200) NOT NULL, |
| 712 | + pr_level NVARCHAR(200) NOT NULL, |
| 713 | + pr_cascade SMALLINT NOT NULL, |
| 714 | + pr_user INT NULL, |
| 715 | + pr_expiry DATETIME NULL, |
| 716 | + pr_id INT UNIQUE IDENTITY, |
| 717 | + CONSTRAINT /*$wgDBprefix*/pr_pagetype PRIMARY KEY(pr_page,pr_type), |
| 718 | +); |
| 719 | +CREATE INDEX /*$wgDBprefix*/pr_page ON /*$wgDBprefix*/page_restrictions(pr_page); |
| 720 | +CREATE INDEX /*$wgDBprefix*/pr_typelevel ON /*$wgDBprefix*/page_restrictions(pr_type,pr_level); |
| 721 | +CREATE INDEX /*$wgDBprefix*/pr_pagelevel ON /*$wgDBprefix*/page_restrictions(pr_level); |
| 722 | +CREATE INDEX /*$wgDBprefix*/pr_cascade ON /*$wgDBprefix*/page_restrictions(pr_cascade); |
| 723 | +; |
| 724 | + |
| 725 | +-- Protected titles - nonexistent pages that have been protected |
| 726 | +CREATE TABLE /*$wgDBprefix*/protected_titles ( |
| 727 | + pt_namespace int NOT NULL, |
| 728 | + pt_title NVARCHAR(255) NOT NULL, |
| 729 | + pt_user int NOT NULL, |
| 730 | + pt_reason NVARCHAR(3555), |
| 731 | + pt_timestamp DATETIME NOT NULL, |
| 732 | + pt_expiry DATETIME NOT NULL default '', |
| 733 | + pt_create_perm NVARCHAR(60) NOT NULL, |
| 734 | + PRIMARY KEY (pt_namespace,pt_title), |
| 735 | +); |
| 736 | +CREATE INDEX /*$wgDBprefix*/pt_timestamp ON /*$wgDBprefix*/protected_titles(pt_timestamp); |
| 737 | +; |
| 738 | + |
| 739 | +-- Name/value pairs indexed by page_id |
| 740 | +CREATE TABLE /*$wgDBprefix*/page_props ( |
| 741 | + pp_page int NOT NULL, |
| 742 | + pp_propname NVARCHAR(60) NOT NULL, |
| 743 | + pp_value NVARCHAR(MAX) NOT NULL, |
| 744 | + PRIMARY KEY (pp_page,pp_propname) |
| 745 | +); |
| 746 | + |
| 747 | +-- A table to log updates, one text key row per update. |
| 748 | +CREATE TABLE /*$wgDBprefix*/updatelog ( |
| 749 | + ul_key NVARCHAR(255) NOT NULL, |
| 750 | + PRIMARY KEY (ul_key) |
| 751 | +); |
| 752 | + |
| 753 | +-- NOTE To enable full text indexing on SQL 2008 you need to create an account FDH$MSSQLSERVER |
| 754 | +-- AND assign a password for the FDHOST process to run under |
| 755 | +-- Once you have assigned a password to that account, you need to run the following stored procedure |
| 756 | +-- replacing XXXXX with the password you used. |
| 757 | +-- EXEC sp_fulltext_resetfdhostaccount @username = 'FDH$MSSQLSERVER', @password = 'XXXXXX' ; |
| 758 | + |
| 759 | + |
| 760 | +--- Add the full-text capabilities, depricated in SQL Server 2005, FTS is enabled on all user created tables by default unless you are using SQL Server 2005 Express |
| 761 | +--sp_fulltext_database 'enable'; |
| 762 | +--sp_fulltext_catalog 'WikiCatalog', 'create' |
| 763 | +--sp_fulltext_table |
| 764 | +--sp_fulltext_column |
| 765 | +--sp_fulltext_table 'Articles', 'activate' |
Index: trunk/phase3/includes/search/SearchMssql.php |
— | — | @@ -0,0 +1,240 @@ |
| 2 | +<?php |
| 3 | +# Copyright (C) 2004 Brion Vibber <brion@pobox.com> |
| 4 | +# http://www.mediawiki.org/ |
| 5 | +# |
| 6 | +# This program is free software; you can redistribute it and/or modify |
| 7 | +# it under the terms of the GNU General Public License as published by |
| 8 | +# the Free Software Foundation; either version 2 of the License, or |
| 9 | +# (at your option) any later version. |
| 10 | +# |
| 11 | +# This program is distributed in the hope that it will be useful, |
| 12 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +# GNU General Public License for more details. |
| 15 | +# |
| 16 | +# You should have received a copy of the GNU General Public License along |
| 17 | +# with this program; if not, write to the Free Software Foundation, Inc., |
| 18 | +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 19 | +# http://www.gnu.org/copyleft/gpl.html |
| 20 | + |
| 21 | +/** |
| 22 | + * Search engine hook base class for Mssql (ConText). |
| 23 | + * @addtogroup Search |
| 24 | + */ |
| 25 | +class SearchMssql extends SearchEngine { |
| 26 | + |
| 27 | + function __construct( $db ) { |
| 28 | + $this->db = $db; |
| 29 | + } |
| 30 | + |
| 31 | + /** |
| 32 | + * Perform a full text search query and return a result set. |
| 33 | + * |
| 34 | + * @param string $term - Raw search term |
| 35 | + * @return MssqlSearchResultSet |
| 36 | + * @access public |
| 37 | + */ |
| 38 | + function searchText( $term ) { |
| 39 | + $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), true ) ) ); |
| 40 | + return new MssqlSearchResultSet( $resultSet, $this->searchTerms ); |
| 41 | + } |
| 42 | + |
| 43 | + /** |
| 44 | + * Perform a title-only search query and return a result set. |
| 45 | + * |
| 46 | + * @param string $term - Raw search term |
| 47 | + * @return MssqlSearchResultSet |
| 48 | + * @access public |
| 49 | + */ |
| 50 | + function searchTitle( $term ) { |
| 51 | + $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), false ) ) ); |
| 52 | + return new MssqlSearchResultSet( $resultSet, $this->searchTerms ); |
| 53 | + } |
| 54 | + |
| 55 | + |
| 56 | + /** |
| 57 | + * Return a partial WHERE clause to exclude redirects, if so set |
| 58 | + * @return string |
| 59 | + * @private |
| 60 | + */ |
| 61 | + function queryRedirect() { |
| 62 | + if ( $this->showRedirects ) { |
| 63 | + return ''; |
| 64 | + } else { |
| 65 | + return 'AND page_is_redirect=0'; |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Return a partial WHERE clause to limit the search to the given namespaces |
| 71 | + * @return string |
| 72 | + * @private |
| 73 | + */ |
| 74 | + function queryNamespaces() { |
| 75 | + $namespaces = implode( ',', $this->namespaces ); |
| 76 | + if ( $namespaces == '' ) { |
| 77 | + $namespaces = '0'; |
| 78 | + } |
| 79 | + return 'AND page_namespace IN (' . $namespaces . ')'; |
| 80 | + } |
| 81 | + |
| 82 | + /** |
| 83 | + * Return a LIMIT clause to limit results on the query. |
| 84 | + * @return string |
| 85 | + * @private |
| 86 | + */ |
| 87 | + function queryLimit( $sql ) { |
| 88 | + return $this->db->limitResult( $sql, $this->limit, $this->offset ); |
| 89 | + } |
| 90 | + |
| 91 | + /** |
| 92 | + * Does not do anything for generic search engine |
| 93 | + * subclasses may define this though |
| 94 | + * @return string |
| 95 | + * @private |
| 96 | + */ |
| 97 | + function queryRanking( $filteredTerm, $fulltext ) { |
| 98 | + return ' ORDER BY ftindex.[RANK] DESC'; // return ' ORDER BY score(1)'; |
| 99 | + } |
| 100 | + |
| 101 | + /** |
| 102 | + * Construct the full SQL query to do the search. |
| 103 | + * The guts shoulds be constructed in queryMain() |
| 104 | + * @param string $filteredTerm |
| 105 | + * @param bool $fulltext |
| 106 | + * @private |
| 107 | + */ |
| 108 | + function getQuery( $filteredTerm, $fulltext ) { |
| 109 | + return $this->queryLimit( $this->queryMain( $filteredTerm, $fulltext ) . ' ' . |
| 110 | + $this->queryRedirect() . ' ' . |
| 111 | + $this->queryNamespaces() . ' ' . |
| 112 | + $this->queryRanking( $filteredTerm, $fulltext ) . ' ' ); |
| 113 | + } |
| 114 | + |
| 115 | + |
| 116 | + /** |
| 117 | + * Picks which field to index on, depending on what type of query. |
| 118 | + * @param bool $fulltext |
| 119 | + * @return string |
| 120 | + */ |
| 121 | + function getIndexField( $fulltext ) { |
| 122 | + return $fulltext ? 'si_text' : 'si_title'; |
| 123 | + } |
| 124 | + |
| 125 | + /** |
| 126 | + * Get the base part of the search query. |
| 127 | + * |
| 128 | + * @param string $filteredTerm |
| 129 | + * @param bool $fulltext |
| 130 | + * @return string |
| 131 | + * @private |
| 132 | + */ |
| 133 | + function queryMain( $filteredTerm, $fulltext ) { |
| 134 | + $match = $this->parseQuery( $filteredTerm, $fulltext ); |
| 135 | + $page = $this->db->tableName( 'page' ); |
| 136 | + $searchindex = $this->db->tableName( 'searchindex' ); |
| 137 | + |
| 138 | + return 'SELECT page_id, page_namespace, page_title, ftindex.[RANK]' . |
| 139 | + "FROM $page,FREETEXTTABLE($searchindex , $match, LANGUAGE 'English') as ftindex " . |
| 140 | + 'WHERE page_id=ftindex.[KEY] '; |
| 141 | + } |
| 142 | + |
| 143 | + /** @todo document */ |
| 144 | + function parseQuery( $filteredText, $fulltext ) { |
| 145 | + global $wgContLang; |
| 146 | + $lc = SearchEngine::legalSearchChars(); |
| 147 | + $this->searchTerms = array(); |
| 148 | + |
| 149 | + # FIXME: This doesn't handle parenthetical expressions. |
| 150 | + $m = array(); |
| 151 | + $q = array(); |
| 152 | + |
| 153 | + if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', |
| 154 | + $filteredText, $m, PREG_SET_ORDER ) ) { |
| 155 | + foreach ( $m as $terms ) { |
| 156 | + $q[] = $terms[1] . $wgContLang->normalizeForSearch( $terms[2] ); |
| 157 | + |
| 158 | + if ( !empty( $terms[3] ) ) { |
| 159 | + $regexp = preg_quote( $terms[3], '/' ); |
| 160 | + if ( $terms[4] ) |
| 161 | + $regexp .= "[0-9A-Za-z_]+"; |
| 162 | + } else { |
| 163 | + $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' ); |
| 164 | + } |
| 165 | + $this->searchTerms[] = $regexp; |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + $searchon = $this->db->strencode( join( ',', $q ) ); |
| 170 | + $field = $this->getIndexField( $fulltext ); |
| 171 | + return "$field, '$searchon'"; |
| 172 | + } |
| 173 | + |
| 174 | + /** |
| 175 | + * Create or update the search index record for the given page. |
| 176 | + * Title and text should be pre-processed. |
| 177 | + * |
| 178 | + * @param int $id |
| 179 | + * @param string $title |
| 180 | + * @param string $text |
| 181 | + */ |
| 182 | + function update( $id, $title, $text ) { |
| 183 | + // We store the column data as UTF-8 byte order marked binary stream |
| 184 | + // because we are invoking the plain text IFilter on it so that, and we want it |
| 185 | + // to properly decode the stream as UTF-8. SQL doesn't support UTF8 as a data type |
| 186 | + // but the indexer will correctly handle it by this method. Since all we are doing |
| 187 | + // is passing this data to the indexer and never retrieving it via PHP, this will save space |
| 188 | + $table = $this->db->tableName( 'searchindex' ); |
| 189 | + $utf8bom = '0xEFBBBF'; |
| 190 | + $si_title = $utf8bom . bin2hex( $title ); |
| 191 | + $si_text = $utf8bom . bin2hex( $text ); |
| 192 | + $sql = "DELETE FROM $table WHERE si_page = $id;"; |
| 193 | + $sql .= "INSERT INTO $table (si_page, si_title, si_text) VALUES ($id, $si_title, $si_text)"; |
| 194 | + return $this->db->query( $sql, 'SearchMssql::update' ); |
| 195 | + } |
| 196 | + |
| 197 | + /** |
| 198 | + * Update a search index record's title only. |
| 199 | + * Title should be pre-processed. |
| 200 | + * |
| 201 | + * @param int $id |
| 202 | + * @param string $title |
| 203 | + */ |
| 204 | + function updateTitle( $id, $title ) { |
| 205 | + $table = $this->db->tableName( 'searchindex' ); |
| 206 | + |
| 207 | + // see update for why we are using the utf8bom |
| 208 | + $utf8bom = '0xEFBBBF'; |
| 209 | + $si_title = $utf8bom . bin2hex( $title ); |
| 210 | + $sql = "DELETE FROM $table WHERE si_page = $id;"; |
| 211 | + $sql .= "INSERT INTO $table (si_page, si_title, si_text) VALUES ($id, $si_title, 0x00)"; |
| 212 | + return $this->db->query( $sql, 'SearchMssql::updateTitle' ); |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +/** |
| 217 | + * @addtogroup Search |
| 218 | + */ |
| 219 | +class MssqlSearchResultSet extends SearchResultSet { |
| 220 | + function __construct( $resultSet, $terms ) { |
| 221 | + $this->mResultSet = $resultSet; |
| 222 | + $this->mTerms = $terms; |
| 223 | + } |
| 224 | + |
| 225 | + function termMatches() { |
| 226 | + return $this->mTerms; |
| 227 | + } |
| 228 | + |
| 229 | + function numRows() { |
| 230 | + return $this->mResultSet->numRows(); |
| 231 | + } |
| 232 | + |
| 233 | + function next() { |
| 234 | + $row = $this->mResultSet->fetchObject(); |
| 235 | + if ( $row === false ) |
| 236 | + return false; |
| 237 | + return new SearchResult( $row ); |
| 238 | + } |
| 239 | +} |
| 240 | + |
| 241 | + |
Index: trunk/phase3/includes/db/Database.php |
— | — | @@ -1878,11 +1878,11 @@ |
1879 | 1879 | } while( $this->wasDeadlock() && --$tries > 0 ); |
1880 | 1880 | $this->ignoreErrors( $oldIgnore ); |
1881 | 1881 | if ( $tries <= 0 ) { |
1882 | | - $this->query( 'ROLLBACK', $myFname ); |
| 1882 | + $this->rollback( $myFname ); |
1883 | 1883 | $this->reportQueryError( $error, $errno, $sql, $fname ); |
1884 | 1884 | return false; |
1885 | 1885 | } else { |
1886 | | - $this->query( 'COMMIT', $myFname ); |
| 1886 | + $this->commit( $myFname ); |
1887 | 1887 | return $retVal; |
1888 | 1888 | } |
1889 | 1889 | } |
Index: trunk/phase3/includes/db/DatabaseMssql.php |
— | — | @@ -0,0 +1,1238 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * This is the MS SQL Server Native database abstraction layer. |
| 6 | + * |
| 7 | + * @addtogroup Database |
| 8 | + * @author Joel Penner <a-joelpe at microsoft dot com> |
| 9 | + * @author Chris Pucci <a-cpucci at microsoft dot com> |
| 10 | + * @author Ryan Biesemeyer <v-ryanbi at microsoft dot com> |
| 11 | + */ |
| 12 | + |
| 13 | +/** |
| 14 | + * @addtogroup Database |
| 15 | + */ |
| 16 | +class DatabaseMssql extends DatabaseBase { |
| 17 | + var $mInsertId = NULL; |
| 18 | + var $mLastResult = NULL; |
| 19 | + var $mAffectedRows = NULL; |
| 20 | + |
| 21 | + function __construct( $server = false, $user = false, $password = false, $dbName = false, |
| 22 | + $failFunction = false, $flags = 0 ) |
| 23 | + { |
| 24 | + $this->mFailFunction = $failFunction; |
| 25 | + $this->mFlags = $flags; |
| 26 | + $this->open( $server, $user, $password, $dbName ); |
| 27 | + |
| 28 | + } |
| 29 | + |
| 30 | + function cascadingDeletes() { |
| 31 | + return true; |
| 32 | + } |
| 33 | + function cleanupTriggers() { |
| 34 | + return true; |
| 35 | + } |
| 36 | + function strictIPs() { |
| 37 | + return true; |
| 38 | + } |
| 39 | + function realTimestamps() { |
| 40 | + return true; |
| 41 | + } |
| 42 | + function implicitGroupby() { |
| 43 | + return false; |
| 44 | + } |
| 45 | + function implicitOrderby() { |
| 46 | + return false; |
| 47 | + } |
| 48 | + function functionalIndexes() { |
| 49 | + return true; |
| 50 | + } |
| 51 | + function unionSupportsOrderAndLimit() { |
| 52 | + return false; |
| 53 | + } |
| 54 | + |
| 55 | + static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) |
| 56 | + { |
| 57 | + return new DatabaseMssql( $server, $user, $password, $dbName, $failFunction, $flags ); |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Usually aborts on failure |
| 62 | + * If the failFunction is set to a non-zero integer, returns success |
| 63 | + */ |
| 64 | + function open( $server, $user, $password, $dbName ) { |
| 65 | + # Test for driver support, to avoid suppressed fatal error |
| 66 | + if ( !function_exists( 'sqlsrv_connect' ) ) { |
| 67 | + throw new DBConnectionError( $this, "MS Sql Server Native (sqlsrv) functions missing. You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" ); |
| 68 | + } |
| 69 | + |
| 70 | + global $wgDBport; |
| 71 | + |
| 72 | + if ( !strlen( $user ) ) { # # e.g. the class is being loaded |
| 73 | + return; |
| 74 | + } |
| 75 | + |
| 76 | + $this->close(); |
| 77 | + $this->mServer = $server; |
| 78 | + $this->mPort = $port = $wgDBport; |
| 79 | + $this->mUser = $user; |
| 80 | + $this->mPassword = $password; |
| 81 | + $this->mDBname = $dbName; |
| 82 | + |
| 83 | + $connectionInfo = array(); |
| 84 | + |
| 85 | + if( $dbName ) { |
| 86 | + $connectionInfo['Database'] = $dbName; |
| 87 | + } |
| 88 | + |
| 89 | + // Start NT Auth Hack |
| 90 | + // Quick and dirty work around to provide NT Auth designation support. |
| 91 | + // Current solution requires installer to know to input 'ntauth' for both username and password |
| 92 | + // to trigger connection via NT Auth. - ugly, ugly, ugly |
| 93 | + // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen. |
| 94 | + $ntAuthUserTest = strtolower( $user ); |
| 95 | + $ntAuthPassTest = strtolower( $password ); |
| 96 | + |
| 97 | + // Decide which auth scenerio to use |
| 98 | + if( ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) ){ |
| 99 | + // Don't add credentials to $connectionInfo |
| 100 | + } else { |
| 101 | + $connectionInfo['UID'] = $user; |
| 102 | + $connectionInfo['PWD'] = $password; |
| 103 | + } |
| 104 | + // End NT Auth Hack |
| 105 | + |
| 106 | + $this->mConn = @sqlsrv_connect( $server, $connectionInfo ); |
| 107 | + |
| 108 | + if ( $this->mConn === false ) { |
| 109 | + wfDebug( "DB connection error\n" ); |
| 110 | + wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); |
| 111 | + wfDebug( $this->lastError() . "\n" ); |
| 112 | + return false; |
| 113 | + } |
| 114 | + |
| 115 | + $this->mOpened = true; |
| 116 | + return $this->mConn; |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Closes a database connection, if it is open |
| 121 | + * Returns success, true if already closed |
| 122 | + */ |
| 123 | + function close() { |
| 124 | + $this->mOpened = false; |
| 125 | + if ( $this->mConn ) { |
| 126 | + return sqlsrv_close( $this->mConn ); |
| 127 | + } else { |
| 128 | + return true; |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + function doQuery( $sql ) { |
| 133 | + wfDebug( "SQL: [$sql]\n" ); |
| 134 | + $this->offset = 0; |
| 135 | + |
| 136 | + // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause |
| 137 | + // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT |
| 138 | + // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to |
| 139 | + // $this->limitResult(); |
| 140 | + if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) { |
| 141 | + // massage LIMIT -> TopN |
| 142 | + $sql = $this->LimitToTopN( $sql ) ; |
| 143 | + } |
| 144 | + |
| 145 | + // MSSQL doesn't have EXTRACT(epoch FROM XXX) |
| 146 | + if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) { |
| 147 | + // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970 |
| 148 | + $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql ); |
| 149 | + } |
| 150 | + |
| 151 | + // perform query |
| 152 | + $stmt = sqlsrv_query( $this->mConn, $sql ); |
| 153 | + if ( $stmt == false ) { |
| 154 | + $message = "A database error has occurred\n" . |
| 155 | + "Query: " . htmlentities( $sql ) . "\n" . |
| 156 | + "Function: " . __FUNCTION__ . "\n"; |
| 157 | + // process each error (our driver will give us an array of errors unlike other providers) |
| 158 | + foreach ( sqlsrv_errors() as $error ) { |
| 159 | + $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n"; |
| 160 | + } |
| 161 | + |
| 162 | + throw new DBUnexpectedError( $this, $message ); |
| 163 | + } |
| 164 | + // remember number of rows affected |
| 165 | + $this->mAffectedRows = sqlsrv_rows_affected( $stmt ); |
| 166 | + |
| 167 | + // if it is a SELECT statement, or an insert with a request to output something we want to return a row. |
| 168 | + if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) || |
| 169 | + ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) { |
| 170 | + // this is essentially a rowset, but Mediawiki calls these 'result' |
| 171 | + // the rowset owns freeing the statement |
| 172 | + $res = new MssqlResult( $stmt ); |
| 173 | + } else { |
| 174 | + // otherwise we simply return it was successful, failure throws an exception |
| 175 | + $res = true; |
| 176 | + } |
| 177 | + return $res; |
| 178 | + } |
| 179 | + |
| 180 | + function freeResult( $res ) { |
| 181 | + if ( $res instanceof ResultWrapper ) { |
| 182 | + $res = $res->result; |
| 183 | + } |
| 184 | + $res->free(); |
| 185 | + } |
| 186 | + |
| 187 | + function fetchObject( $res ) { |
| 188 | + if ( $res instanceof ResultWrapper ) { |
| 189 | + $res = $res->result; |
| 190 | + } |
| 191 | + $row = $res->fetch( 'OBJECT' ); |
| 192 | + return $row; |
| 193 | + } |
| 194 | + |
| 195 | + function getErrors() { |
| 196 | + $strRet = ''; |
| 197 | + $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL ); |
| 198 | + if ( $retErrors != null ) { |
| 199 | + foreach ( $retErrors as $arrError ) { |
| 200 | + $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n"; |
| 201 | + $strRet .= "Error Code: " . $arrError[ 'code'] . "\n"; |
| 202 | + $strRet .= "Message: " . $arrError[ 'message'] . "\n"; |
| 203 | + } |
| 204 | + } else { |
| 205 | + $strRet = "No errors found"; |
| 206 | + } |
| 207 | + return $strRet; |
| 208 | + } |
| 209 | + |
| 210 | + function fetchRow( $res ) { |
| 211 | + if ( $res instanceof ResultWrapper ) { |
| 212 | + $res = $res->result; |
| 213 | + } |
| 214 | + $row = $res->fetch( SQLSRV_FETCH_BOTH ); |
| 215 | + return $row; |
| 216 | + } |
| 217 | + |
| 218 | + function numRows( $res ) { |
| 219 | + if ( $res instanceof ResultWrapper ) { |
| 220 | + $res = $res->result; |
| 221 | + } |
| 222 | + return ( $res ) ? $res->numrows() : 0; |
| 223 | + } |
| 224 | + |
| 225 | + function numFields( $res ) { |
| 226 | + if ( $res instanceof ResultWrapper ) { |
| 227 | + $res = $res->result; |
| 228 | + } |
| 229 | + return ( $res ) ? $res->numfields() : 0; |
| 230 | + } |
| 231 | + |
| 232 | + function fieldName( $res, $n ) { |
| 233 | + if ( $res instanceof ResultWrapper ) { |
| 234 | + $res = $res->result; |
| 235 | + } |
| 236 | + return ( $res ) ? $res->fieldname( $n ) : 0; |
| 237 | + } |
| 238 | + |
| 239 | + /** |
| 240 | + * This must be called after nextSequenceVal |
| 241 | + */ |
| 242 | + function insertId() { |
| 243 | + return $this->mInsertId; |
| 244 | + } |
| 245 | + |
| 246 | + function dataSeek( $res, $row ) { |
| 247 | + if ( $res instanceof ResultWrapper ) { |
| 248 | + $res = $res->result; |
| 249 | + } |
| 250 | + return ( $res ) ? $res->seek( $row ) : false; |
| 251 | + } |
| 252 | + |
| 253 | + function lastError() { |
| 254 | + if ( $this->mConn ) { |
| 255 | + return $this->getErrors(); |
| 256 | + } |
| 257 | + else { |
| 258 | + return "No database connection"; |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + function lastErrno() { |
| 263 | + $err = sqlsrv_errors( SQLSRV_ERR_ALL ); |
| 264 | + if ( $err[0] ) return $err[0]['code']; |
| 265 | + else return 0; |
| 266 | + } |
| 267 | + |
| 268 | + function affectedRows() { |
| 269 | + return $this->mAffectedRows; |
| 270 | + } |
| 271 | + |
| 272 | + /** |
| 273 | + * SELECT wrapper |
| 274 | + * |
| 275 | + * @param mixed $table Array or string, table name(s) (prefix auto-added) |
| 276 | + * @param mixed $vars Array or string, field name(s) to be retrieved |
| 277 | + * @param mixed $conds Array or string, condition(s) for WHERE |
| 278 | + * @param string $fname Calling function name (use __METHOD__) for logs/profiling |
| 279 | + * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), |
| 280 | + * see Database::makeSelectOptions code for list of supported stuff |
| 281 | + * @param $join_conds Array: Associative array of table join conditions (optional) |
| 282 | + * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) |
| 283 | + * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure |
| 284 | + */ |
| 285 | + function select( $table, $vars, $conds = '', $fname = 'Database::select', $options = array(), $join_conds = array() ) |
| 286 | + { |
| 287 | + $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); |
| 288 | + if ( isset( $options['EXPLAIN'] ) ) { |
| 289 | + sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" ); |
| 290 | + return $this->query( $sql, $fname ); |
| 291 | + sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" ); |
| 292 | + } |
| 293 | + return $this->query( $sql, $fname ); |
| 294 | + } |
| 295 | + |
| 296 | + /** |
| 297 | + * SELECT wrapper |
| 298 | + * |
| 299 | + * @param $table Mixed: Array or string, table name(s) (prefix auto-added) |
| 300 | + * @param $vars Mixed: Array or string, field name(s) to be retrieved |
| 301 | + * @param $conds Mixed: Array or string, condition(s) for WHERE |
| 302 | + * @param $fname String: Calling function name (use __METHOD__) for logs/profiling |
| 303 | + * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), |
| 304 | + * see Database::makeSelectOptions code for list of supported stuff |
| 305 | + * @param $join_conds Array: Associative array of table join conditions (optional) |
| 306 | + * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) |
| 307 | + * @return string, the SQL text |
| 308 | + */ |
| 309 | + function selectSQLText( $table, $vars, $conds = '', $fname = 'Database::select', $options = array(), $join_conds = array() ) { |
| 310 | + if ( isset( $options['EXPLAIN'] ) ) { |
| 311 | + unset( $options['EXPLAIN'] ); |
| 312 | + } |
| 313 | + return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); |
| 314 | + } |
| 315 | + |
| 316 | + /** |
| 317 | + * Estimate rows in dataset |
| 318 | + * Returns estimated count, based on SHOWPLAN_ALL output |
| 319 | + * This is not necessarily an accurate estimate, so use sparingly |
| 320 | + * Returns -1 if count cannot be found |
| 321 | + * Takes same arguments as Database::select() |
| 322 | + */ |
| 323 | + function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'Database::estimateRowCount', $options = array() ) { |
| 324 | + $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx |
| 325 | + $res = $this->select( $table, $vars, $conds, $fname, $options ); |
| 326 | + |
| 327 | + $rows = -1; |
| 328 | + if ( $res ) { |
| 329 | + $row = $this->fetchRow( $res ); |
| 330 | + if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows']; |
| 331 | + $this->freeResult( $res ); |
| 332 | + } |
| 333 | + return $rows; |
| 334 | + } |
| 335 | + |
| 336 | + |
| 337 | + /** |
| 338 | + * Returns information about an index |
| 339 | + * If errors are explicitly ignored, returns NULL on failure |
| 340 | + */ |
| 341 | + function indexInfo( $table, $index, $fname = 'Database::indexExists' ) { |
| 342 | + # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the |
| 343 | + # returned value except to check for the existance of indexes. |
| 344 | + $sql = "sp_helpindex '" . $table . "'"; |
| 345 | + $res = $this->query( $sql, $fname ); |
| 346 | + if ( !$res ) { |
| 347 | + return NULL; |
| 348 | + } |
| 349 | + |
| 350 | + $result = array(); |
| 351 | + while ( $row = $res->FetchNextObj() ) { |
| 352 | + if ( $row->index_name == $index ) { |
| 353 | + $row->Non_unique = !stristr( $row->index_description, "unique" ); |
| 354 | + $cols = explode( ", ", $row->index_keys ); |
| 355 | + foreach ( $cols as $col ) { |
| 356 | + $row->Column_name = trim( $col ); |
| 357 | + $result[] = clone $row; |
| 358 | + } |
| 359 | + } else if ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) { |
| 360 | + $row->Non_unique = 0; |
| 361 | + $cols = explode( ", ", $row->index_keys ); |
| 362 | + foreach ( $cols as $col ) { |
| 363 | + $row->Column_name = trim( $col ); |
| 364 | + $result[] = clone $row; |
| 365 | + } |
| 366 | + } |
| 367 | + } |
| 368 | + return empty( $result ) ? false : $result; |
| 369 | + } |
| 370 | + |
| 371 | + /** |
| 372 | + * INSERT wrapper, inserts an array into a table |
| 373 | + * |
| 374 | + * $arrToInsert may be a single associative array, or an array of these with numeric keys, for |
| 375 | + * multi-row insert. |
| 376 | + * |
| 377 | + * Usually aborts on failure |
| 378 | + * If errors are explicitly ignored, returns success |
| 379 | + */ |
| 380 | + function insert( $table, $arrToInsert, $fname = 'Database::insert', $options = array() ) { |
| 381 | + # No rows to insert, easy just return now |
| 382 | + if ( !count( $arrToInsert ) ) { |
| 383 | + return true; |
| 384 | + } |
| 385 | + |
| 386 | + if ( !is_array( $options ) ) { |
| 387 | + $options = array( $options ); |
| 388 | + } |
| 389 | + |
| 390 | + $table = $this->tableName( $table ); |
| 391 | + |
| 392 | + if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row |
| 393 | + $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible |
| 394 | + } |
| 395 | + |
| 396 | + $allOk = true; |
| 397 | + |
| 398 | + |
| 399 | + // We know the table we're inserting into, get its identity column |
| 400 | + $identity = null; |
| 401 | + $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name |
| 402 | + $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" ); |
| 403 | + if( $res && $res->numrows() ){ |
| 404 | + // There is an identity for this table. |
| 405 | + $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) ); |
| 406 | + } |
| 407 | + unset( $res ); |
| 408 | + |
| 409 | + foreach ( $arrToInsert as $blah => $a ) { |
| 410 | + // start out with empty identity column, this is so we can return it as a result of the insert logic |
| 411 | + $sqlPre = ''; |
| 412 | + $sqlPost = ''; |
| 413 | + $identityClause = ''; |
| 414 | + |
| 415 | + // if we have an identity column |
| 416 | + if( $identity ) { |
| 417 | + // iterate through |
| 418 | + foreach ($a as $k => $v ) { |
| 419 | + if ( $k == $identity ) { |
| 420 | + if( !is_null($v) ){ |
| 421 | + // there is a value being passed to us, we need to turn on and off inserted identity |
| 422 | + $sqlPre = "SET IDENTITY_INSERT $table ON;" ; |
| 423 | + $sqlPost = ";SET IDENTITY_INSERT $table OFF;"; |
| 424 | + |
| 425 | + } else { |
| 426 | + // we can't insert NULL into an identity column, so remove the column from the insert. |
| 427 | + unset( $a[$k] ); |
| 428 | + } |
| 429 | + } |
| 430 | + } |
| 431 | + $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result |
| 432 | + } |
| 433 | + |
| 434 | + $keys = array_keys( $a ); |
| 435 | + |
| 436 | + |
| 437 | + // INSERT IGNORE is not supported by SQL Server |
| 438 | + // remove IGNORE from options list and set ignore flag to true |
| 439 | + $ignoreClause = false; |
| 440 | + foreach ( $options as $k => $v ) { |
| 441 | + if ( strtoupper( $v ) == "IGNORE" ) { |
| 442 | + unset( $options[$k] ); |
| 443 | + $ignoreClause = true; |
| 444 | + } |
| 445 | + } |
| 446 | + |
| 447 | + // translate MySQL INSERT IGNORE to something SQL Server can use |
| 448 | + // example: |
| 449 | + // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop') |
| 450 | + // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop') |
| 451 | + if ( $ignoreClause == true ) { |
| 452 | + $prival = $a[$keys[0]]; |
| 453 | + $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')"; |
| 454 | + } |
| 455 | + |
| 456 | + // Build the actual query |
| 457 | + $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) . |
| 458 | + " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES ("; |
| 459 | + |
| 460 | + $first = true; |
| 461 | + foreach ( $a as $key => $value ) { |
| 462 | + if ( $first ) { |
| 463 | + $first = false; |
| 464 | + } else { |
| 465 | + $sql .= ','; |
| 466 | + } |
| 467 | + if ( is_string( $value ) ) { |
| 468 | + $sql .= $this->quote_ident( $value ); |
| 469 | + } elseif ( is_null( $value ) ) { |
| 470 | + $sql .= 'null'; |
| 471 | + } elseif ( is_array( $value ) || is_object( $value ) ) { |
| 472 | + if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) { |
| 473 | + $sql .= $this->quote_ident( $value->fetch() ); |
| 474 | + } else { |
| 475 | + $sql .= $this->quote_ident( serialize( $value ) ); |
| 476 | + } |
| 477 | + } else { |
| 478 | + $sql .= $value; |
| 479 | + } |
| 480 | + } |
| 481 | + $sql .= ')' . $sqlPost; |
| 482 | + |
| 483 | + // Run the query |
| 484 | + $ret = sqlsrv_query( $this->mConn, $sql ); |
| 485 | + |
| 486 | + if ( $ret === false ) { |
| 487 | + throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname ); |
| 488 | + } elseif ( $ret != NULL ) { |
| 489 | + // remember number of rows affected |
| 490 | + $this->mAffectedRows = sqlsrv_rows_affected( $ret ); |
| 491 | + if ( !is_null($identity) ) { |
| 492 | + // then we want to get the identity column value we were assigned and save it off |
| 493 | + $row = sqlsrv_fetch_object( $ret ); |
| 494 | + $this->mInsertId = $row->$identity; |
| 495 | + } |
| 496 | + sqlsrv_free_stmt( $ret ); |
| 497 | + continue; |
| 498 | + } |
| 499 | + $allOk = false; |
| 500 | + } |
| 501 | + return $allOk; |
| 502 | + } |
| 503 | + |
| 504 | + /** |
| 505 | + * INSERT SELECT wrapper |
| 506 | + * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) |
| 507 | + * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() |
| 508 | + * $conds may be "*" to copy the whole table |
| 509 | + * srcTable may be an array of tables. |
| 510 | + */ |
| 511 | + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect', |
| 512 | + $insertOptions = array(), $selectOptions = array() ) |
| 513 | + { |
| 514 | + $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, insertOptions, $selectOptions ); |
| 515 | + |
| 516 | + if ( $ret === false ) { |
| 517 | + throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname ); |
| 518 | + } elseif ( $ret != NULL ) { |
| 519 | + // remember number of rows affected |
| 520 | + $this->mAffectedRows = sqlsrv_rows_affected( $ret ); |
| 521 | + return $ret; |
| 522 | + } |
| 523 | + return NULL; |
| 524 | + } |
| 525 | + |
| 526 | + /** |
| 527 | + * Format a table name ready for use in constructing an SQL query |
| 528 | + * |
| 529 | + * This does two important things: it brackets table names which as necessary, |
| 530 | + * and it adds a table prefix if there is one. |
| 531 | + * |
| 532 | + * All functions of this object which require a table name call this function |
| 533 | + * themselves. Pass the canonical name to such functions. This is only needed |
| 534 | + * when calling query() directly. |
| 535 | + * |
| 536 | + * @param string $name database table name |
| 537 | + */ |
| 538 | + function tableName( $name ) { |
| 539 | + global $wgSharedDB; |
| 540 | + # Skip quoted literals |
| 541 | + if ( $name != '' && $name { 0 } != '[' ) { |
| 542 | + if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) { |
| 543 | + $name = "{$this->mTablePrefix}$name"; |
| 544 | + } |
| 545 | + if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) { |
| 546 | + $name = "[$wgSharedDB].[$name]"; |
| 547 | + } else { |
| 548 | + # Standard quoting |
| 549 | + if ( $name != '' ) $name = "[$name]"; |
| 550 | + } |
| 551 | + } |
| 552 | + return $name; |
| 553 | + } |
| 554 | + |
| 555 | + /** |
| 556 | + * Return the next in a sequence, save the value for retrieval via insertId() |
| 557 | + */ |
| 558 | + function nextSequenceValue( $seqName ) { |
| 559 | + if ( !$this->tableExists( 'sequence_' . $seqName ) ) { |
| 560 | + $ret = sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" ); |
| 561 | + } |
| 562 | + $ret = sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" ); |
| 563 | + $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" ); |
| 564 | + $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't |
| 565 | + |
| 566 | + sqlsrv_free_stmt( $ret ); |
| 567 | + $this->mInsertId = $row['id']; |
| 568 | + return $row['id']; |
| 569 | + } |
| 570 | + |
| 571 | + /** |
| 572 | + * Return the current value of a sequence. Assumes it has ben nextval'ed in this session. |
| 573 | + */ |
| 574 | + function currentSequenceValue( $seqName ) { |
| 575 | + $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" ); |
| 576 | + if ( $ret !== false ) { |
| 577 | + $row = sqlsrv_fetch_array( $ret ); |
| 578 | + sqlsrv_free_stmt( $ret ); |
| 579 | + return $row['id']; |
| 580 | + } else { |
| 581 | + return $this->nextSequenceValue( $seqName ); |
| 582 | + } |
| 583 | + } |
| 584 | + |
| 585 | + |
| 586 | + # REPLACE query wrapper |
| 587 | + # MSSQL simulates this with a DELETE followed by INSERT |
| 588 | + # $row is the row to insert, an associative array |
| 589 | + # $uniqueIndexes is an array of indexes. Each element may be either a |
| 590 | + # field name or an array of field names |
| 591 | + # |
| 592 | + # It may be more efficient to leave off unique indexes which are unlikely to collide. |
| 593 | + # However if you do this, you run the risk of encountering errors which wouldn't have |
| 594 | + # occurred in MySQL |
| 595 | + function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) { |
| 596 | + $table = $this->tableName( $table ); |
| 597 | + |
| 598 | + if ( count( $rows ) == 0 ) { |
| 599 | + return; |
| 600 | + } |
| 601 | + |
| 602 | + # Single row case |
| 603 | + if ( !is_array( reset( $rows ) ) ) { |
| 604 | + $rows = array( $rows ); |
| 605 | + } |
| 606 | + |
| 607 | + foreach ( $rows as $row ) { |
| 608 | + # Delete rows which collide |
| 609 | + if ( $uniqueIndexes ) { |
| 610 | + $sql = "DELETE FROM $table WHERE "; |
| 611 | + $first = true; |
| 612 | + foreach ( $uniqueIndexes as $index ) { |
| 613 | + if ( $first ) { |
| 614 | + $first = false; |
| 615 | + $sql .= "("; |
| 616 | + } else { |
| 617 | + $sql .= ') OR ('; |
| 618 | + } |
| 619 | + if ( is_array( $index ) ) { |
| 620 | + $first2 = true; |
| 621 | + foreach ( $index as $col ) { |
| 622 | + if ( $first2 ) { |
| 623 | + $first2 = false; |
| 624 | + } else { |
| 625 | + $sql .= ' AND '; |
| 626 | + } |
| 627 | + $sql .= $col . '=' . $this->addQuotes( $row[$col] ); |
| 628 | + } |
| 629 | + } else { |
| 630 | + $sql .= $index . '=' . $this->addQuotes( $row[$index] ); |
| 631 | + } |
| 632 | + } |
| 633 | + $sql .= ')'; |
| 634 | + $this->query( $sql, $fname ); |
| 635 | + } |
| 636 | + |
| 637 | + # Now insert the row |
| 638 | + $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) . ') VALUES (' . |
| 639 | + $this->makeList( $row, LIST_COMMA ) . ')'; |
| 640 | + $this->query( $sql, $fname ); |
| 641 | + } |
| 642 | + } |
| 643 | + |
| 644 | + # DELETE where the condition is a join |
| 645 | + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) { |
| 646 | + if ( !$conds ) { |
| 647 | + throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' ); |
| 648 | + } |
| 649 | + |
| 650 | + $delTable = $this->tableName( $delTable ); |
| 651 | + $joinTable = $this->tableName( $joinTable ); |
| 652 | + $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; |
| 653 | + if ( $conds != '*' ) { |
| 654 | + $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); |
| 655 | + } |
| 656 | + $sql .= ')'; |
| 657 | + |
| 658 | + $this->query( $sql, $fname ); |
| 659 | + } |
| 660 | + |
| 661 | + # Returns the size of a text field, or -1 for "unlimited" |
| 662 | + function textFieldSize( $table, $field ) { |
| 663 | + $table = $this->tableName( $table ); |
| 664 | + $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns |
| 665 | + WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'"; |
| 666 | + $res = $this->query( $sql ); |
| 667 | + $row = $this->fetchRow( $res ); |
| 668 | + $size = -1; |
| 669 | + if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) $size = $row['CHARACTER_MAXIMUM_LENGTH']; |
| 670 | + $this->freeResult( $res ); |
| 671 | + return $size; |
| 672 | + } |
| 673 | + |
| 674 | + /** |
| 675 | + * Construct a LIMIT query with optional offset |
| 676 | + * This is used for query pages |
| 677 | + * $sql string SQL query we will append the limit too |
| 678 | + * $limit integer the SQL limit |
| 679 | + * $offset integer the SQL offset (default false) |
| 680 | + */ |
| 681 | + function limitResult( $sql, $limit, $offset = false ) { |
| 682 | + if ( $offset === false || $offset == 0 ) { |
| 683 | + if ( strpos( $sql, "SELECT" ) === false ) { |
| 684 | + return "TOP {$limit} " . $sql; |
| 685 | + } else { |
| 686 | + return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 ); |
| 687 | + } |
| 688 | + } else { |
| 689 | + $sql = ' |
| 690 | + SELECT * FROM ( |
| 691 | + SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM ( |
| 692 | + SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1 |
| 693 | + ) as sub2 |
| 694 | + ) AS sub3 |
| 695 | + WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit ); |
| 696 | + return $sql; |
| 697 | + } |
| 698 | + } |
| 699 | + |
| 700 | + // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult() |
| 701 | + // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser. |
| 702 | + // This exists becase there are still too many extensions that don't use dynamic sql generation. |
| 703 | + function LimitToTopN( $sql ) { |
| 704 | + // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset} |
| 705 | + $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i'; |
| 706 | + if ( preg_match( $pattern, $sql, $matches ) ) { |
| 707 | + // row_count = $matches[4] |
| 708 | + $row_count = $matches[4]; |
| 709 | + // offset = $matches[3] OR $matches[6] |
| 710 | + $offset = $matches[3] or |
| 711 | + $offset = $matches[6] or |
| 712 | + $offset = false; |
| 713 | + |
| 714 | + // strip the matching LIMIT clause out |
| 715 | + $sql = str_replace( $matches[0], '', $sql ); |
| 716 | + return $this->limitResult( $sql, $row_count, $offset ); |
| 717 | + } |
| 718 | + return $sql; |
| 719 | + } |
| 720 | + |
| 721 | + // MSSQL does support this, but documentation is too thin to make a generalized |
| 722 | + // function for this. Apparently UPDATE TOP (N) works, but the sort order |
| 723 | + // may not be what we're expecting so the top n results may be a random selection. |
| 724 | + // TODO: Implement properly. |
| 725 | + function limitResultForUpdate( $sql, $num ) { |
| 726 | + return $sql; |
| 727 | + } |
| 728 | + |
| 729 | + |
| 730 | + function timestamp( $ts = 0 ) { |
| 731 | + return wfTimestamp( TS_ISO_8601, $ts ); |
| 732 | + } |
| 733 | + |
| 734 | + /** |
| 735 | + * @return string wikitext of a link to the server software's web site |
| 736 | + */ |
| 737 | + function getSoftwareLink() { |
| 738 | + return "[http://www.microsoft.com/sql/ MS SQL Server]"; |
| 739 | + } |
| 740 | + |
| 741 | + /** |
| 742 | + * @return string Version information from the database |
| 743 | + */ |
| 744 | + function getServerVersion() { |
| 745 | + $server_info = sqlsrv_server_info( $this->mConn ); |
| 746 | + $version = 'Error'; |
| 747 | + if ( isset( $server_info['SQLServerVersion'] ) ) $version = $server_info['SQLServerVersion']; |
| 748 | + return $version; |
| 749 | + } |
| 750 | + |
| 751 | + function tableExists ( $table, $schema = false ) { |
| 752 | + $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables |
| 753 | + WHERE table_type='BASE TABLE' AND table_name = '$table'" ); |
| 754 | + if ( $res === false ) { |
| 755 | + print( "Error in tableExists query: " . $this->getErrors() ); |
| 756 | + return false; |
| 757 | + } |
| 758 | + if ( sqlsrv_fetch( $res ) ) |
| 759 | + return true; |
| 760 | + else |
| 761 | + return false; |
| 762 | + } |
| 763 | + |
| 764 | + /** |
| 765 | + * Query whether a given column exists in the mediawiki schema |
| 766 | + */ |
| 767 | + function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) { |
| 768 | + $table = $this->tableName( $table ); |
| 769 | + $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns |
| 770 | + WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); |
| 771 | + if ( $res === false ) { |
| 772 | + print( "Error in fieldExists query: " . $this->getErrors() ); |
| 773 | + return false; |
| 774 | + } |
| 775 | + if ( sqlsrv_fetch( $res ) ) |
| 776 | + return true; |
| 777 | + else |
| 778 | + return false; |
| 779 | + } |
| 780 | + |
| 781 | + function fieldInfo( $table, $field ) { |
| 782 | + $table = $this->tableName( $table ); |
| 783 | + $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns |
| 784 | + WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); |
| 785 | + if ( $res === false ) { |
| 786 | + print( "Error in fieldInfo query: " . $this->getErrors() ); |
| 787 | + return false; |
| 788 | + } |
| 789 | + if ( $meta = $this->fetchRow( $res ) ) |
| 790 | + return new MssqlField( $meta ); |
| 791 | + return false; |
| 792 | + } |
| 793 | + |
| 794 | + /** |
| 795 | + * Begin a transaction, committing any previously open transaction |
| 796 | + */ |
| 797 | + function begin( $fname = 'Database::begin' ) { |
| 798 | + sqlsrv_begin_transaction( $this->mConn ); |
| 799 | + $this->mTrxLevel = 1; |
| 800 | + } |
| 801 | + |
| 802 | + /** |
| 803 | + * End a transaction |
| 804 | + */ |
| 805 | + function commit( $fname = 'Database::commit' ) { |
| 806 | + sqlsrv_commit( $this->mConn ); |
| 807 | + $this->mTrxLevel = 0; |
| 808 | + } |
| 809 | + |
| 810 | + /** |
| 811 | + * Rollback a transaction. |
| 812 | + * No-op on non-transactional databases. |
| 813 | + */ |
| 814 | + function rollback( $fname = 'Database::rollback' ) { |
| 815 | + sqlsrv_rollback( $this->mConn ); |
| 816 | + $this->mTrxLevel = 0; |
| 817 | + } |
| 818 | + |
| 819 | + function setup_database() { |
| 820 | + global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser; |
| 821 | + |
| 822 | + // Make sure that we can write to the correct schema |
| 823 | + $ctest = "mediawiki_test_table"; |
| 824 | + if ( $this->tableExists( $ctest ) ) { |
| 825 | + $this->doQuery( "DROP TABLE $ctest" ); |
| 826 | + } |
| 827 | + $SQL = "CREATE TABLE $ctest (a int)"; |
| 828 | + $res = $this->doQuery( $SQL ); |
| 829 | + if ( !$res ) { |
| 830 | + print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the database</li>\n"; |
| 831 | + dieout( "</ul>" ); |
| 832 | + } |
| 833 | + $this->doQuery( "DROP TABLE $ctest" ); |
| 834 | + |
| 835 | + $res = dbsource( "../maintenance/mssql/tables.sql", $this ); |
| 836 | + |
| 837 | + # # Update version information |
| 838 | + $mwv = $this->addQuotes( $wgVersion ); |
| 839 | + $pgv = $this->addQuotes( $this->getServerVersion() ); |
| 840 | + $pgu = $this->addQuotes( $this->mUser ); |
| 841 | + $pgp = $this->addQuotes( $wgDBport ); |
| 842 | + $dbn = $this->addQuotes( $this->mDBname ); |
| 843 | + |
| 844 | + # # Avoid the non-standard "REPLACE INTO" syntax |
| 845 | + $f = fopen( "../maintenance/interwiki.sql", 'r' ); |
| 846 | + if ( $f == false ) { |
| 847 | + dieout( "<li>Could not find the interwiki.sql file" ); |
| 848 | + } |
| 849 | + # # We simply assume it is already empty as we have just created it |
| 850 | + $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; |
| 851 | + while ( ! feof( $f ) ) { |
| 852 | + $line = fgets( $f, 1024 ); |
| 853 | + $matches = array(); |
| 854 | + if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) { |
| 855 | + continue; |
| 856 | + } |
| 857 | + $this->query( "$SQL $matches[1],$matches[2])" ); |
| 858 | + } |
| 859 | + print " (table interwiki successfully populated)...\n"; |
| 860 | + |
| 861 | + $this->commit(); |
| 862 | + } |
| 863 | + |
| 864 | + /** |
| 865 | + * Initial setup as superuser. |
| 866 | + * Create the database, schema, login, and user. |
| 867 | + */ |
| 868 | + function initial_setup() { |
| 869 | + global $conf; |
| 870 | + |
| 871 | + // FIXME: fields need to be properly escaped. |
| 872 | + |
| 873 | + $this->doQuery("CREATE DATABASE {$conf->DBname};"); |
| 874 | + $this->doQuery("USE {$conf->DBname};"); |
| 875 | + $this->doQuery("CREATE SCHEMA {$conf->DBname};"); |
| 876 | + $this->doQuery(" |
| 877 | + CREATE |
| 878 | + LOGIN {$conf->DBuser} |
| 879 | + WITH |
| 880 | + PASSWORD='{$conf->DBpassword}' |
| 881 | + ; |
| 882 | + "); |
| 883 | + $this->doQuery(" |
| 884 | + CREATE |
| 885 | + USER {$conf->DBuser} |
| 886 | + FOR |
| 887 | + LOGIN {$conf->DBuser} |
| 888 | + WITH |
| 889 | + DEFAULT_SCHEMA={$conf->DBname} |
| 890 | + ; |
| 891 | + "); |
| 892 | + $this->doQuery(" |
| 893 | + GRANT |
| 894 | + BACKUP DATABASE, |
| 895 | + BACKUP LOG, |
| 896 | + CREATE DEFAULT, |
| 897 | + CREATE FUNCTION, |
| 898 | + CREATE PROCEDURE, |
| 899 | + CREATE RULE, |
| 900 | + CREATE TABLE, |
| 901 | + CREATE VIEW, |
| 902 | + CREATE FULLTEXT CATALOG |
| 903 | + ON |
| 904 | + DATABASE::{$conf->DBname} |
| 905 | + TO {$conf->DBuser} |
| 906 | + ; |
| 907 | + "); |
| 908 | + $this->doQuery(" |
| 909 | + GRANT |
| 910 | + CONTROL |
| 911 | + ON |
| 912 | + SCHEMA::{$conf->DBname} |
| 913 | + TO {$conf->DBuser} |
| 914 | + ; |
| 915 | + "); |
| 916 | + |
| 917 | + |
| 918 | + } |
| 919 | + |
| 920 | + function encodeBlob( $b ) { |
| 921 | + // we can't have zero's and such, this is a simple encoding to make sure we don't barf |
| 922 | + return base64_encode( $b ); |
| 923 | + } |
| 924 | + |
| 925 | + function decodeBlob( $b ) { |
| 926 | + // we can't have zero's and such, this is a simple encoding to make sure we don't barf |
| 927 | + return base64_decode( $b ); |
| 928 | + } |
| 929 | + |
| 930 | + /** |
| 931 | + * @private |
| 932 | + */ |
| 933 | + function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) { |
| 934 | + $ret = array(); |
| 935 | + $retJOIN = array(); |
| 936 | + $use_index_safe = is_array( $use_index ) ? $use_index : array(); |
| 937 | + $join_conds_safe = is_array( $join_conds ) ? $join_conds : array(); |
| 938 | + foreach ( $tables as $table ) { |
| 939 | + // Is there a JOIN and INDEX clause for this table? |
| 940 | + if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) { |
| 941 | + $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table ); |
| 942 | + $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) ); |
| 943 | + $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')'; |
| 944 | + $retJOIN[] = $tableClause; |
| 945 | + // Is there an INDEX clause? |
| 946 | + } else if ( isset( $use_index_safe[$table] ) ) { |
| 947 | + $tableClause = $this->tableName( $table ); |
| 948 | + $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) ); |
| 949 | + $ret[] = $tableClause; |
| 950 | + // Is there a JOIN clause? |
| 951 | + } else if ( isset( $join_conds_safe[$table] ) ) { |
| 952 | + $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table ); |
| 953 | + $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')'; |
| 954 | + $retJOIN[] = $tableClause; |
| 955 | + } else { |
| 956 | + $tableClause = $this->tableName( $table ); |
| 957 | + $ret[] = $tableClause; |
| 958 | + } |
| 959 | + } |
| 960 | + // We can't separate explicit JOIN clauses with ',', use ' ' for those |
| 961 | + $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : ""; |
| 962 | + $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : ""; |
| 963 | + // Compile our final table clause |
| 964 | + return implode( ' ', array( $straightJoins, $otherJoins ) ); |
| 965 | + } |
| 966 | + |
| 967 | + function strencode( $s ) { # # Should not be called by us |
| 968 | + return str_replace( "'", "''", $s ); |
| 969 | + } |
| 970 | + |
| 971 | + function addQuotes( $s ) { |
| 972 | + if ( $s instanceof Blob ) { |
| 973 | + return "'" . $s->fetch( $s ) . "'"; |
| 974 | + } else { |
| 975 | + return parent::addQuotes( $s ); |
| 976 | + } |
| 977 | + } |
| 978 | + |
| 979 | + function quote_ident( $s ) { |
| 980 | + return "'" . str_replace( "'", "''", $s ) . "'"; |
| 981 | + } |
| 982 | + |
| 983 | + function selectDB( $db ) { |
| 984 | + return ( $this->query( "SET DATABASE $db" ) !== false ); |
| 985 | + } |
| 986 | + |
| 987 | + /** |
| 988 | + * @private |
| 989 | + * |
| 990 | + * @param array $options an associative array of options to be turned into |
| 991 | + * an SQL query, valid keys are listed in the function. |
| 992 | + * @return array |
| 993 | + */ |
| 994 | + function makeSelectOptions( $options ) { |
| 995 | + $tailOpts = ''; |
| 996 | + $startOpts = ''; |
| 997 | + |
| 998 | + $noKeyOptions = array(); |
| 999 | + foreach ( $options as $key => $option ) { |
| 1000 | + if ( is_numeric( $key ) ) { |
| 1001 | + $noKeyOptions[$option] = true; |
| 1002 | + } |
| 1003 | + } |
| 1004 | + |
| 1005 | + if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}"; |
| 1006 | + if ( isset( $options['HAVING'] ) ) $tailOpts .= " HAVING {$options['GROUP BY']}"; |
| 1007 | + if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}"; |
| 1008 | + |
| 1009 | + if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; |
| 1010 | + |
| 1011 | + // we want this to be compatible with the output of parent::makeSelectOptions() |
| 1012 | + return array( $startOpts, '' , $tailOpts, '' ); |
| 1013 | + } |
| 1014 | + |
| 1015 | + public function setTimeout( $timeout ) { |
| 1016 | + // couldn't find any timeout functions |
| 1017 | + } |
| 1018 | + |
| 1019 | + function ping() { |
| 1020 | + wfDebug( "Function ping() not written for MSSQL Native yet" ); |
| 1021 | + return true; |
| 1022 | + } |
| 1023 | + |
| 1024 | + /** |
| 1025 | + * How lagged is this slave? |
| 1026 | + * |
| 1027 | + */ |
| 1028 | + public function getLag() { |
| 1029 | + # Not implemented for MSSQL |
| 1030 | + return false; |
| 1031 | + } |
| 1032 | + |
| 1033 | + /** |
| 1034 | + * FIXME: Add lag support |
| 1035 | + */ |
| 1036 | + function setFakeSlaveLag( $lag ) { } |
| 1037 | + function setFakeMaster( $enabled = false ) { } |
| 1038 | + |
| 1039 | + /** |
| 1040 | + * Get the type of the DBMS, as it appears in $wgDBtype. |
| 1041 | + */ |
| 1042 | + function getType(){ |
| 1043 | + return 'mssql'; |
| 1044 | + } |
| 1045 | + |
| 1046 | + function buildConcat( $stringList ) { |
| 1047 | + return implode( ' + ', $stringList ); |
| 1048 | + } |
| 1049 | + |
| 1050 | + public function getSearchEngine() { |
| 1051 | + return "SearchMssql"; |
| 1052 | + } |
| 1053 | + |
| 1054 | +} // end DatabaseMssql class |
| 1055 | + |
| 1056 | +/** |
| 1057 | + * Utility class. |
| 1058 | + * @addtogroup Database |
| 1059 | + */ |
| 1060 | +class MssqlField { |
| 1061 | + private $name, $tablename, $default, $max_length, $nullable, $type; |
| 1062 | + function __construct ( $info ) { |
| 1063 | + $this->name = $info['COLUMN_NAME']; |
| 1064 | + $this->tablename = $info['TABLE_NAME']; |
| 1065 | + $this->default = $info['COLUMN_DEFAULT']; |
| 1066 | + $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; |
| 1067 | + $this->nullable = ( strtolower( $info['IS_NULLABLE'] ) == 'no' ) ? false:true; |
| 1068 | + $this->type = $info['DATA_TYPE']; |
| 1069 | + } |
| 1070 | + function name() { |
| 1071 | + return $this->name; |
| 1072 | + } |
| 1073 | + |
| 1074 | + function tableName() { |
| 1075 | + return $this->tableName; |
| 1076 | + } |
| 1077 | + |
| 1078 | + function defaultValue() { |
| 1079 | + return $this->default; |
| 1080 | + } |
| 1081 | + |
| 1082 | + function maxLength() { |
| 1083 | + return $this->max_length; |
| 1084 | + } |
| 1085 | + |
| 1086 | + function nullable() { |
| 1087 | + return $this->nullable; |
| 1088 | + } |
| 1089 | + |
| 1090 | + function type() { |
| 1091 | + return $this->type; |
| 1092 | + } |
| 1093 | +} |
| 1094 | + |
| 1095 | +/** |
| 1096 | + * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows into an array and maintain our |
| 1097 | + * own cursor index into that array...This is similar to the way the Oracle driver handles this same issue |
| 1098 | + * |
| 1099 | + * @addtogroup Database |
| 1100 | + */ |
| 1101 | +class MssqlResult { |
| 1102 | + |
| 1103 | + public function __construct( $queryresult = false ) { |
| 1104 | + $this->mCursor = 0; |
| 1105 | + $this->mRows = array(); |
| 1106 | + $this->mNumFields = sqlsrv_num_fields( $queryresult ); |
| 1107 | + $this->mFieldMeta = sqlsrv_field_metadata( $queryresult ); |
| 1108 | + while ( $row = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ) ) { |
| 1109 | + if ( $row !== null ) { |
| 1110 | + foreach ( $row as $k => $v ) { |
| 1111 | + if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object |
| 1112 | + $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" ); |
| 1113 | + } |
| 1114 | + } |
| 1115 | + $this->mRows[] = $row;// read results into memory, cursors are not supported |
| 1116 | + } |
| 1117 | + } |
| 1118 | + $this->mRowCount = count( $this->mRows ); |
| 1119 | + sqlsrv_free_stmt( $queryresult ); |
| 1120 | + } |
| 1121 | + |
| 1122 | + private function array_to_obj( $array, &$obj ) { |
| 1123 | + foreach ( $array as $key => $value ) { |
| 1124 | + if ( is_array( $value ) ) { |
| 1125 | + $obj->$key = new stdClass(); |
| 1126 | + array_to_obj( $value, $obj->$key ); |
| 1127 | + } else { |
| 1128 | + if ( !empty( $key ) ) { |
| 1129 | + $obj->$key = $value; |
| 1130 | + } |
| 1131 | + } |
| 1132 | + } |
| 1133 | + return $obj; |
| 1134 | + } |
| 1135 | + |
| 1136 | + public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) { |
| 1137 | + if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) return false; |
| 1138 | + $ret = false; |
| 1139 | + $arrNum = array(); |
| 1140 | + if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) { |
| 1141 | + foreach ( $this->mRows[$this->mCursor] as $key => $value ) { |
| 1142 | + $arrNum[] = $value; |
| 1143 | + } |
| 1144 | + } |
| 1145 | + switch( $mode ) { |
| 1146 | + case SQLSRV_FETCH_ASSOC: |
| 1147 | + $ret = $this->mRows[$this->mCursor]; |
| 1148 | + break; |
| 1149 | + case SQLSRV_FETCH_NUMERIC: |
| 1150 | + $ret = $arrNum; |
| 1151 | + break; |
| 1152 | + case 'OBJECT': |
| 1153 | + $o = new $object_class; |
| 1154 | + $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o ); |
| 1155 | + break; |
| 1156 | + case SQLSRV_FETCH_BOTH: |
| 1157 | + default: |
| 1158 | + $ret = $this->mRows[$this->mCursor] + $arrNum; |
| 1159 | + break; |
| 1160 | + } |
| 1161 | + |
| 1162 | + $this->mCursor++; |
| 1163 | + return $ret; |
| 1164 | + } |
| 1165 | + |
| 1166 | + public function get( $pos, $fld ) { |
| 1167 | + return $this->mRows[$pos][$fld]; |
| 1168 | + } |
| 1169 | + |
| 1170 | + public function numrows() { |
| 1171 | + return $this->mRowCount; |
| 1172 | + } |
| 1173 | + |
| 1174 | + public function seek( $iRow ) { |
| 1175 | + $this->mCursor = min( $iRow, $this->mRowCount ); |
| 1176 | + } |
| 1177 | + |
| 1178 | + public function numfields() { |
| 1179 | + return $this->mNumFields; |
| 1180 | + } |
| 1181 | + |
| 1182 | + public function fieldname( $nr ) { |
| 1183 | + $arrKeys = array_keys( $this->mRows[0] ); |
| 1184 | + return $arrKeys[$nr]; |
| 1185 | + } |
| 1186 | + |
| 1187 | + public function fieldtype( $nr ) { |
| 1188 | + $i = 0; |
| 1189 | + $intType = -1; |
| 1190 | + $strType = ''; |
| 1191 | + foreach ( $this->mFieldMeta as $meta ) { |
| 1192 | + if ( $nr == $i ) { |
| 1193 | + $intType = $meta['Type']; |
| 1194 | + break; |
| 1195 | + } |
| 1196 | + $i++; |
| 1197 | + } |
| 1198 | + // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table |
| 1199 | + switch( $intType ) { |
| 1200 | + case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break; |
| 1201 | + case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break; |
| 1202 | + case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break; |
| 1203 | + case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break; |
| 1204 | + case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break; |
| 1205 | + case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break; |
| 1206 | + case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break; |
| 1207 | + case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break; |
| 1208 | + case SQLSRV_SQLTYPE_INT: $strType = 'int'; break; |
| 1209 | + case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break; |
| 1210 | + case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break; |
| 1211 | + case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break; |
| 1212 | + case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break; |
| 1213 | + // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break; |
| 1214 | + case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break; |
| 1215 | + case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break; |
| 1216 | + case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break; |
| 1217 | + case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break; |
| 1218 | + case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break; |
| 1219 | + case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break; |
| 1220 | + case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break; |
| 1221 | + case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break; |
| 1222 | + case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break; |
| 1223 | + case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break; |
| 1224 | + case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break; |
| 1225 | + // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break; |
| 1226 | + case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break; |
| 1227 | + // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break; |
| 1228 | + case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break; |
| 1229 | + default: $strType = $intType; |
| 1230 | + } |
| 1231 | + return $strType; |
| 1232 | + } |
| 1233 | + |
| 1234 | + public function free() { |
| 1235 | + unset( $this->mRows ); |
| 1236 | + return; |
| 1237 | + } |
| 1238 | + |
| 1239 | +} |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -343,6 +343,7 @@ |
344 | 344 | 'ChronologyProtector' => 'includes/db/LBFactory.php', |
345 | 345 | 'Database' => 'includes/db/DatabaseMysql.php', |
346 | 346 | 'DatabaseBase' => 'includes/db/Database.php', |
| 347 | + 'DatabaseMssql' => 'includes/db/DatabaseMssql.php', |
347 | 348 | 'DatabaseMysql' => 'includes/db/DatabaseMysql.php', |
348 | 349 | 'DatabaseOracle' => 'includes/db/DatabaseOracle.php', |
349 | 350 | 'DatabasePostgres' => 'includes/db/DatabasePostgres.php', |
— | — | @@ -511,6 +512,7 @@ |
512 | 513 | 'SearchEngine' => 'includes/search/SearchEngine.php', |
513 | 514 | 'SearchHighlighter' => 'includes/search/SearchEngine.php', |
514 | 515 | 'SearchIBM_DB2' => 'includes/search/SearchIBM_DB2.php', |
| 516 | + 'SearchMssql' => 'includes/search/SearchMssql.php', |
515 | 517 | 'SearchMySQL4' => 'includes/search/SearchMySQL4.php', |
516 | 518 | 'SearchMySQL' => 'includes/search/SearchMySQL.php', |
517 | 519 | 'SearchOracle' => 'includes/search/SearchOracle.php', |
Index: trunk/phase3/includes/specials/SpecialUnusedimages.php |
— | — | @@ -53,6 +53,9 @@ |
54 | 54 | case 'sqlite': |
55 | 55 | $epoch = 'img_timestamp'; |
56 | 56 | break; |
| 57 | + case 'mssql': |
| 58 | + $epoch = 'DATEDIFF(s,CONVERT(datetime,\'1/1/1970\'),img_timestamp)'; |
| 59 | + break; |
57 | 60 | default: |
58 | 61 | $epoch = 'EXTRACT(epoch FROM img_timestamp)'; |
59 | 62 | } |
Index: trunk/phase3/includes/specials/SpecialAncientpages.php |
— | — | @@ -54,6 +54,9 @@ |
55 | 55 | case 'sqlite': |
56 | 56 | $epoch = 'rev_timestamp'; |
57 | 57 | break; |
| 58 | + case 'mssql': |
| 59 | + $epoch = 'DATEDIFF(s,CONVERT(datetime,\'1/1/1970\'),rev_timestamp)'; |
| 60 | + break; |
58 | 61 | default: |
59 | 62 | $epoch = 'EXTRACT(epoch FROM rev_timestamp)'; |
60 | 63 | } |
Index: trunk/phase3/includes/specials/SpecialSearch.php |
— | — | @@ -119,7 +119,41 @@ |
120 | 120 | wfProfileIn( __METHOD__ ); |
121 | 121 | |
122 | 122 | $sk = $wgUser->getSkin(); |
123 | | - |
| 123 | + |
| 124 | + $beginSearchForm = Xml::openElement( |
| 125 | + 'form', |
| 126 | + array( |
| 127 | + 'id' => ( $this->searchAdvanced ? 'powersearch' : 'search' ), |
| 128 | + 'method' => 'get', |
| 129 | + 'action' => $wgScript |
| 130 | + ) |
| 131 | + ); |
| 132 | + $mwSearchTopTable = |
| 133 | + Xml::openElement( 'table', array( 'id' => 'mw-search-top-table', 'border' => 0, 'cellpadding' => 0, 'cellspacing' => 0 ) ) . |
| 134 | + Xml::openElement( 'tr' ) . |
| 135 | + Xml::openElement( 'td' ) . "\n" . |
| 136 | + $this->shortDialog( $term ) . |
| 137 | + Xml::closeElement( 'td' ) . |
| 138 | + Xml::closeElement( 'tr' ) . |
| 139 | + Xml::closeElement( 'table' ); |
| 140 | + |
| 141 | + // moved to check for empty or null search string before running query |
| 142 | + // to prevent NULL fulltext search error in SQL Server |
| 143 | + $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; |
| 144 | + if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) { |
| 145 | + $wgOut->addHTML( $beginSearchForm ); |
| 146 | + $wgOut->addHTML( $mwSearchTopTable ); |
| 147 | + $wgOut->addHTML( $this->searchFocus() ); |
| 148 | + $wgOut->addHTML( $this->formHeader( $term, 0, 0 ) ); |
| 149 | + if ( $this->searchAdvanced ) { |
| 150 | + $wgOut->addHTML( $this->powerSearchBox( $term ) ); |
| 151 | + } |
| 152 | + $wgOut->addHTML( Xml::closeElement( 'form' ) ); |
| 153 | + // Empty query -- straight view of search form |
| 154 | + wfProfileOut( __METHOD__ ); |
| 155 | + return; |
| 156 | + } |
| 157 | + |
124 | 158 | $this->searchEngine = SearchEngine::create(); |
125 | 159 | $search =& $this->searchEngine; |
126 | 160 | $search->setLimitOffset( $this->limit, $this->offset ); |
— | — | @@ -194,23 +228,10 @@ |
195 | 229 | } |
196 | 230 | // start rendering the page |
197 | 231 | $wgOut->addHtml( |
198 | | - Xml::openElement( |
199 | | - 'form', |
200 | | - array( |
201 | | - 'id' => ( $this->searchAdvanced ? 'powersearch' : 'search' ), |
202 | | - 'method' => 'get', |
203 | | - 'action' => $wgScript |
204 | | - ) |
205 | | - ) |
| 232 | + $beginSearchForm |
206 | 233 | ); |
207 | 234 | $wgOut->addHtml( |
208 | | - Xml::openElement( 'table', array( 'id'=>'mw-search-top-table', 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) . |
209 | | - Xml::openElement( 'tr' ) . |
210 | | - Xml::openElement( 'td' ) . "\n" . |
211 | | - $this->shortDialog( $term ) . |
212 | | - Xml::closeElement('td') . |
213 | | - Xml::closeElement('tr') . |
214 | | - Xml::closeElement('table') |
| 235 | + $mwSearchTopTable |
215 | 236 | ); |
216 | 237 | |
217 | 238 | // Sometimes the search engine knows there are too many hits |
— | — | @@ -220,19 +241,6 @@ |
221 | 242 | return; |
222 | 243 | } |
223 | 244 | |
224 | | - $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':'; |
225 | | - if( trim( $term ) === '' || $filePrefix === trim( $term ) ) { |
226 | | - $wgOut->addHTML( $this->searchFocus() ); |
227 | | - $wgOut->addHTML( $this->formHeader($term, 0, 0)); |
228 | | - if( $this->searchAdvanced ) { |
229 | | - $wgOut->addHTML( $this->powerSearchBox( $term ) ); |
230 | | - } |
231 | | - $wgOut->addHTML( '</form>' ); |
232 | | - // Empty query -- straight view of search form |
233 | | - wfProfileOut( __METHOD__ ); |
234 | | - return; |
235 | | - } |
236 | | - |
237 | 245 | // Get number of results |
238 | 246 | $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0; |
239 | 247 | $textMatchesNum = $textMatches ? $textMatches->numRows() : 0; |
Index: trunk/phase3/includes/Block.php |
— | — | @@ -857,6 +857,16 @@ |
858 | 858 | public static function infinity() { |
859 | 859 | # This is a special keyword for timestamps in PostgreSQL, and |
860 | 860 | # works with CHAR(14) as well because "i" sorts after all numbers. |
| 861 | + |
| 862 | + # BEGIN DatabaseMssql hack |
| 863 | + # Since MSSQL doesn't recognize the infinity keyword, set date manually. |
| 864 | + # TO-DO: Refactor for better DB portability and remove magic date |
| 865 | + $dbw = wfGetDB( DB_MASTER ); |
| 866 | + if ( $dbw instanceof DatabaseMssql ) { |
| 867 | + return '3000-01-31 00:00:00.000'; |
| 868 | + } |
| 869 | + # End DatabaseMssql hack |
| 870 | + |
861 | 871 | return 'infinity'; |
862 | 872 | } |
863 | 873 | |
Index: trunk/phase3/config/Installer.php |
— | — | @@ -87,6 +87,15 @@ |
88 | 88 | 'serverless' => true |
89 | 89 | ); |
90 | 90 | |
| 91 | +$ourdb['mssql'] = array( |
| 92 | + 'fullname' => 'Microsoft SQL Server', |
| 93 | + 'havedriver' => 0, |
| 94 | + 'compile' => 'sqlsrv', |
| 95 | + 'bgcolor' => '#cccccc', |
| 96 | + 'rootuser' => 'root', |
| 97 | + 'serverless' => false |
| 98 | +); |
| 99 | + |
91 | 100 | $ourdb['ibm_db2'] = array( |
92 | 101 | 'fullname' => 'DB2', |
93 | 102 | 'havedriver' => 0, |
— | — | @@ -910,6 +919,44 @@ |
911 | 920 | |
912 | 921 | if( !$ok ) { continue; } |
913 | 922 | } |
| 923 | + else if ( $conf->DBtype == 'mssql' ) { |
| 924 | + error_reporting( E_ALL ); |
| 925 | + $wgSuperUser = ''; |
| 926 | + # # Possible connect as a superuser |
| 927 | + if ( $useRoot ) { |
| 928 | + $wgDBsuperuser = $conf->RootUser; |
| 929 | + echo( "<li>Attempting to connect to database \"{$conf->DBtype}\" as superuser \"{$wgDBsuperuser}\"" ); |
| 930 | + $wgDatabase = $dbc->newFromParams( |
| 931 | + $conf->DBserver, |
| 932 | + $conf->RootUser, |
| 933 | + $conf->RootPW, |
| 934 | + false, |
| 935 | + false, |
| 936 | + 1 |
| 937 | + ); |
| 938 | + if ( !$wgDatabase->isOpen() ) { |
| 939 | + echo( " error: {$wgDatabase->lastError()}</li>\n" ); |
| 940 | + $errs['DBserver'] = 'Could not connect to database as superuser'; |
| 941 | + $errs['RootUser'] = 'Check username'; |
| 942 | + $errs['RootPW'] = 'and password'; |
| 943 | + continue; |
| 944 | + } |
| 945 | + $wgDatabase->initial_setup( $conf->RootPW, $conf->DBtype ); |
| 946 | + } |
| 947 | + echo( "<li>Attempting to connect to database \"{$wgDBname}\" as \"{$wgDBuser}\"..." ); |
| 948 | + $wgDatabase = $dbc->newFromParams( |
| 949 | + $conf->DBserver, |
| 950 | + $conf->DBuser, |
| 951 | + $conf->DBpassword, |
| 952 | + $conf->DBname, |
| 953 | + 1 |
| 954 | + ); |
| 955 | + if ( !$wgDatabase->isOpen() ) { |
| 956 | + echo( " error: {$wgDatabase->lastError()} </li>\n" ); |
| 957 | + } else { |
| 958 | + $myver = $wgDatabase->getServerVersion(); |
| 959 | + } |
| 960 | + } |
914 | 961 | else if( $conf->DBtype == 'ibm_db2' ) { |
915 | 962 | if( $useRoot ) { |
916 | 963 | $db_user = $conf->RootUser; |
— | — | @@ -1659,6 +1706,13 @@ |
1660 | 1707 | </div> |
1661 | 1708 | </fieldset> |
1662 | 1709 | |
| 1710 | + |
| 1711 | + <?php database_switcher( 'mssql' ); ?> |
| 1712 | + <div class="config-desc"> |
| 1713 | + <p>No MS SQL Server specific options at this time.</p> |
| 1714 | + </div> |
| 1715 | + |
| 1716 | + |
1663 | 1717 | <?php database_switcher('ibm_db2'); ?> |
1664 | 1718 | <div class="config-input"><?php |
1665 | 1719 | aField( $conf, "DBport_db2", "Database port:" ); |