MDL-11082 Improved groups upgrade performance 1.8x -> 1.9; thanks Eloy for telling...
[moodle-pu.git] / lib / adodb / adodb-active-record.inc.php
blob29b4da2062d3eef967f659bee9f1bdedfa03dbd1
1 <?php
2 /*
4 @version V4.94 23 Jan 2007 (c) 2000-2007 John Lim (jlim#natsoft.com.my). All rights reserved.
5 Latest version is available at http://adodb.sourceforge.net
7 Released under both BSD license and Lesser GPL library license.
8 Whenever there is any discrepancy between the two licenses,
9 the BSD license will take precedence.
11 Active Record implementation. Superset of Zend Framework's.
13 Version 0.07
15 See http://www-128.ibm.com/developerworks/java/library/j-cb03076/?ca=dgr-lnxw01ActiveRecord
16 for info on Ruby on Rails Active Record implementation
19 global $_ADODB_ACTIVE_DBS;
20 global $ADODB_ACTIVE_CACHESECS; // set to true to enable caching of metadata such as field info
21 global $ACTIVE_RECORD_SAFETY; // set to false to disable safety checks
23 // array of ADODB_Active_DB's, indexed by ADODB_Active_Record->_dbat
24 $_ADODB_ACTIVE_DBS = array();
25 $ACTIVE_RECORD_SAFETY = true;
27 class ADODB_Active_DB {
28 var $db; // ADOConnection
29 var $tables; // assoc array of ADODB_Active_Table objects, indexed by tablename
32 class ADODB_Active_Table {
33 var $name; // table name
34 var $flds; // assoc array of adofieldobjs, indexed by fieldname
35 var $keys; // assoc array of primary keys, indexed by fieldname
36 var $_created; // only used when stored as a cached file
39 // returns index into $_ADODB_ACTIVE_DBS
40 function ADODB_SetDatabaseAdapter(&$db)
42 global $_ADODB_ACTIVE_DBS;
44 foreach($_ADODB_ACTIVE_DBS as $k => $d) {
45 if (PHP_VERSION >= 5) {
46 if ($d->db === $db) return $k;
47 } else {
48 if ($d->db->_connectionID === $db->_connectionID && $db->database == $d->db->database)
49 return $k;
53 $obj = new ADODB_Active_DB();
54 $obj->db =& $db;
55 $obj->tables = array();
57 $_ADODB_ACTIVE_DBS[] = $obj;
59 return sizeof($_ADODB_ACTIVE_DBS)-1;
63 class ADODB_Active_Record {
64 var $_dbat; // associative index pointing to ADODB_Active_DB eg. $ADODB_Active_DBS[_dbat]
65 var $_table; // tablename, if set in class definition then use it as table name
66 var $_tableat; // associative index pointing to ADODB_Active_Table, eg $ADODB_Active_DBS[_dbat]->tables[$this->_tableat]
67 var $_where; // where clause set in Load()
68 var $_saved = false; // indicates whether data is already inserted.
69 var $_lasterr = false; // last error message
70 var $_original = false; // the original values loaded or inserted, refreshed on update
72 // should be static
73 function SetDatabaseAdapter(&$db)
75 return ADODB_SetDatabaseAdapter($db);
78 // php4 constructor
79 function ADODB_Active_Record($table = false, $pkeyarr=false, $db=false)
81 ADODB_Active_Record::__construct($table,$pkeyarr,$db);
84 // php5 constructor
85 function __construct($table = false, $pkeyarr=false, $db=false)
87 global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS;
89 if ($db == false && is_object($pkeyarr)) {
90 $db = $pkeyarr;
91 $pkeyarr = false;
94 if (!$table) {
95 if (!empty($this->_table)) $table = $this->_table;
96 else $table = $this->_pluralize(get_class($this));
98 if ($db) {
99 $this->_dbat = ADODB_Active_Record::SetDatabaseAdapter($db);
100 } else
101 $this->_dbat = sizeof($_ADODB_ACTIVE_DBS)-1;
104 if ($this->_dbat < 0) $this->Error("No database connection set; use ADOdb_Active_Record::SetDatabaseAdapter(\$db)",'ADODB_Active_Record::__constructor');
106 $this->_table = $table;
107 $this->_tableat = $table; # reserved for setting the assoc value to a non-table name, eg. the sql string in future
108 $this->UpdateActiveTable($pkeyarr);
111 function __wakeup()
113 $class = get_class($this);
114 new $class;
117 function _pluralize($table)
119 $ut = strtoupper($table);
120 $len = strlen($table);
121 $lastc = $ut[$len-1];
122 $lastc2 = substr($ut,$len-2);
123 switch ($lastc) {
124 case 'S':
125 return $table.'es';
126 case 'Y':
127 return substr($table,0,$len-1).'ies';
128 case 'X':
129 return $table.'es';
130 case 'H':
131 if ($lastc2 == 'CH' || $lastc2 == 'SH')
132 return $table.'es';
133 default:
134 return $table.'s';
138 //////////////////////////////////
140 // update metadata
141 function UpdateActiveTable($pkeys=false,$forceUpdate=false)
143 global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS , $ADODB_CACHE_DIR, $ADODB_ACTIVE_CACHESECS;
145 $activedb =& $_ADODB_ACTIVE_DBS[$this->_dbat];
147 $table = $this->_table;
148 $tables = $activedb->tables;
149 $tableat = $this->_tableat;
150 if (!$forceUpdate && !empty($tables[$tableat])) {
151 $tobj =& $tables[$tableat];
152 foreach($tobj->flds as $name => $fld)
153 $this->$name = null;
154 return;
157 $db =& $activedb->db;
158 $fname = $ADODB_CACHE_DIR . '/adodb_' . $db->databaseType . '_active_'. $table . '.cache';
159 if (!$forceUpdate && $ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR && file_exists($fname)) {
160 $fp = fopen($fname,'r');
161 @flock($fp, LOCK_SH);
162 $acttab = unserialize(fread($fp,100000));
163 fclose($fp);
164 if ($acttab->_created + $ADODB_ACTIVE_CACHESECS - (abs(rand()) % 16) > time()) {
165 // abs(rand()) randomizes deletion, reducing contention to delete/refresh file
166 // ideally, you should cache at least 32 secs
167 $activedb->tables[$table] = $acttab;
169 //if ($db->debug) ADOConnection::outp("Reading cached active record file: $fname");
170 return;
171 } else if ($db->debug) {
172 ADOConnection::outp("Refreshing cached active record file: $fname");
175 $activetab = new ADODB_Active_Table();
176 $activetab->name = $table;
179 $cols = $db->MetaColumns($table);
180 if (!$cols) {
181 $this->Error("Invalid table name: $table",'UpdateActiveTable');
182 return false;
184 $fld = reset($cols);
185 if (!$pkeys) {
186 if (isset($fld->primary_key)) {
187 $pkeys = array();
188 foreach($cols as $name => $fld) {
189 if (!empty($fld->primary_key)) $pkeys[] = $name;
191 } else
192 $pkeys = $this->GetPrimaryKeys($db, $table);
194 if (empty($pkeys)) {
195 $this->Error("No primary key found for table $table",'UpdateActiveTable');
196 return false;
199 $attr = array();
200 $keys = array();
202 switch($ADODB_ASSOC_CASE) {
203 case 0:
204 foreach($cols as $name => $fldobj) {
205 $name = strtolower($name);
206 $this->$name = null;
207 $attr[$name] = $fldobj;
209 foreach($pkeys as $k => $name) {
210 $keys[strtolower($name)] = strtolower($name);
212 break;
214 case 1:
215 foreach($cols as $name => $fldobj) {
216 $name = strtoupper($name);
217 $this->$name = null;
218 $attr[$name] = $fldobj;
221 foreach($pkeys as $k => $name) {
222 $keys[strtoupper($name)] = strtoupper($name);
224 break;
225 default:
226 foreach($cols as $name => $fldobj) {
227 $name = ($fldobj->name);
228 $this->$name = null;
229 $attr[$name] = $fldobj;
231 foreach($pkeys as $k => $name) {
232 $keys[$name] = $cols[$name]->name;
234 break;
237 $activetab->keys = $keys;
238 $activetab->flds = $attr;
240 if ($ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR) {
241 $activetab->_created = time();
242 $s = serialize($activetab);
243 if (!function_exists('adodb_write_file')) include(ADODB_DIR.'/adodb-csvlib.inc.php');
244 adodb_write_file($fname,$s);
246 $activedb->tables[$table] = $activetab;
249 function GetPrimaryKeys(&$db, $table)
251 return $db->MetaPrimaryKeys($table);
254 // error handler for both PHP4+5.
255 function Error($err,$fn)
257 global $_ADODB_ACTIVE_DBS;
259 $fn = get_class($this).'::'.$fn;
260 $this->_lasterr = $fn.': '.$err;
262 if ($this->_dbat < 0) $db = false;
263 else {
264 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
265 $db =& $activedb->db;
268 if (function_exists('adodb_throw')) {
269 if (!$db) adodb_throw('ADOdb_Active_Record', $fn, -1, $err, 0, 0, false);
270 else adodb_throw($db->databaseType, $fn, -1, $err, 0, 0, $db);
271 } else
272 if (!$db || $db->debug) ADOConnection::outp($this->_lasterr);
276 // return last error message
277 function ErrorMsg()
279 if (!function_exists('adodb_throw')) {
280 if ($this->_dbat < 0) $db = false;
281 else $db = $this->DB();
283 // last error could be database error too
284 if ($db && $db->ErrorMsg()) return $db->ErrorMsg();
286 return $this->_lasterr;
289 function ErrorNo()
291 if ($this->_dbat < 0) return -9999; // no database connection...
292 $db = $this->DB();
294 return (int) $db->ErrorNo();
298 // retrieve ADOConnection from _ADODB_Active_DBs
299 function &DB()
301 global $_ADODB_ACTIVE_DBS;
303 if ($this->_dbat < 0) {
304 $false = false;
305 $this->Error("No database connection set: use ADOdb_Active_Record::SetDatabaseAdaptor(\$db)", "DB");
306 return $false;
308 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
309 $db =& $activedb->db;
310 return $db;
313 // retrieve ADODB_Active_Table
314 function &TableInfo()
316 global $_ADODB_ACTIVE_DBS;
318 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
319 $table =& $activedb->tables[$this->_tableat];
320 return $table;
323 // set a numeric array (using natural table field ordering) as object properties
324 function Set(&$row)
326 global $ACTIVE_RECORD_SAFETY;
328 $db =& $this->DB();
330 if (!$row) {
331 $this->_saved = false;
332 return false;
335 $this->_saved = true;
337 $table =& $this->TableInfo();
338 if ($ACTIVE_RECORD_SAFETY && sizeof($table->flds) != sizeof($row)) {
339 $this->Error("Table structure of $this->_table has changed","Load");
340 return false;
343 $cnt = 0;
344 foreach($table->flds as $name=>$fld) {
345 $this->$name = $row[$cnt];
346 $cnt += 1;
348 $this->_original = $row;
349 return true;
352 // get last inserted id for INSERT
353 function LastInsertID(&$db,$fieldname)
355 if ($db->hasInsertID)
356 $val = $db->Insert_ID($this->_table,$fieldname);
357 else
358 $val = false;
360 if (is_null($val) || $val === false) {
361 // this might not work reliably in multi-user environment
362 return $db->GetOne("select max(".$fieldname.") from ".$this->_table);
364 return $val;
367 // quote data in where clause
368 function doquote(&$db, $val,$t)
370 switch($t) {
371 case 'D':
372 case 'T':
373 if (empty($val)) return 'null';
375 case 'C':
376 case 'X':
377 if (is_null($val)) return 'null';
379 if (strncmp($val,"'",1) != 0 && substr($val,strlen($val)-1,1) != "'") {
380 return $db->qstr($val);
381 break;
383 default:
384 return $val;
385 break;
389 // generate where clause for an UPDATE/SELECT
390 function GenWhere(&$db, &$table)
392 $keys = $table->keys;
393 $parr = array();
395 foreach($keys as $k) {
396 $f = $table->flds[$k];
397 if ($f) {
398 $parr[] = $k.' = '.$this->doquote($db,$this->$k,$db->MetaType($f->type));
401 return implode(' and ', $parr);
405 //------------------------------------------------------------ Public functions below
407 function Load($where,$bindarr=false)
409 $db =& $this->DB(); if (!$db) return false;
410 $this->_where = $where;
412 $save = $db->SetFetchMode(ADODB_FETCH_NUM);
413 $row = $db->GetRow("select * from ".$this->_table.' WHERE '.$where,$bindarr);
414 $db->SetFetchMode($save);
416 return $this->Set($row);
419 // false on error
420 function Save()
422 if ($this->_saved) $ok = $this->Update();
423 else $ok = $this->Insert();
425 return $ok;
428 // false on error
429 function Insert()
431 $db =& $this->DB(); if (!$db) return false;
432 $cnt = 0;
433 $table =& $this->TableInfo();
435 $valarr = array();
436 $names = array();
437 $valstr = array();
439 foreach($table->flds as $name=>$fld) {
440 $val = $this->$name;
441 if(!is_null($val) || !array_key_exists($name, $table->keys)) {
442 $valarr[] = $val;
443 $names[] = $name;
444 $valstr[] = $db->Param($cnt);
445 $cnt += 1;
449 if (empty($names)){
450 foreach($table->flds as $name=>$fld) {
451 $valarr[] = null;
452 $names[] = $name;
453 $valstr[] = $db->Param($cnt);
454 $cnt += 1;
457 $sql = 'INSERT INTO '.$this->_table."(".implode(',',$names).') VALUES ('.implode(',',$valstr).')';
458 $ok = $db->Execute($sql,$valarr);
460 if ($ok) {
461 $this->_saved = true;
462 $autoinc = false;
463 foreach($table->keys as $k) {
464 if (is_null($this->$k)) {
465 $autoinc = true;
466 break;
469 if ($autoinc && sizeof($table->keys) == 1) {
470 $k = reset($table->keys);
471 $this->$k = $this->LastInsertID($db,$k);
475 $this->_original = $valarr;
476 return !empty($ok);
479 function Delete()
481 $db =& $this->DB(); if (!$db) return false;
482 $table =& $this->TableInfo();
484 $where = $this->GenWhere($db,$table);
485 $sql = 'DELETE FROM '.$this->_table.' WHERE '.$where;
486 $ok = $db->Execute($sql);
488 return $ok ? true : false;
491 // returns an array of active record objects
492 function &Find($whereOrderBy,$bindarr=false,$pkeysArr=false)
494 $db =& $this->DB(); if (!$db || empty($this->_table)) return false;
495 $arr =& $db->GetActiveRecordsClass(get_class($this),$this->_table, $whereOrderBy,$bindarr,$pkeysArr);
496 return $arr;
499 // returns 0 on error, 1 on update, 2 on insert
500 function Replace()
502 global $ADODB_ASSOC_CASE;
504 $db =& $this->DB(); if (!$db) return false;
505 $table =& $this->TableInfo();
507 $pkey = $table->keys;
509 foreach($table->flds as $name=>$fld) {
510 $val = $this->$name;
512 if (is_null($val)) {
513 if (isset($fld->not_null) && $fld->not_null) {
514 if (isset($fld->default_value) && strlen($fld->default_value)) continue;
515 else {
516 $this->Error("Cannot update null into $name","Replace");
517 return false;
521 if (is_null($val) && !empty($fld->auto_increment)) {
522 continue;
524 $t = $db->MetaType($fld->type);
525 $arr[$name] = $this->doquote($db,$val,$t);
526 $valarr[] = $val;
529 if (!is_array($pkey)) $pkey = array($pkey);
532 if ($ADODB_ASSOC_CASE == 0)
533 foreach($pkey as $k => $v)
534 $pkey[$k] = strtolower($v);
535 elseif ($ADODB_ASSOC_CASE == 0)
536 foreach($pkey as $k => $v)
537 $pkey[$k] = strtoupper($v);
539 $ok = $db->Replace($this->_table,$arr,$pkey);
540 if ($ok) {
541 $this->_saved = true; // 1= update 2=insert
542 if ($ok == 2) {
543 $autoinc = false;
544 foreach($table->keys as $k) {
545 if (is_null($this->$k)) {
546 $autoinc = true;
547 break;
550 if ($autoinc && sizeof($table->keys) == 1) {
551 $k = reset($table->keys);
552 $this->$k = $this->LastInsertID($db,$k);
556 $this->_original =& $valarr;
558 return $ok;
561 // returns 0 on error, 1 on update, -1 if no change in data (no update)
562 function Update()
564 $db =& $this->DB(); if (!$db) return false;
565 $table =& $this->TableInfo();
567 $where = $this->GenWhere($db, $table);
569 if (!$where) {
570 $this->error("Where missing for table $table", "Update");
571 return false;
573 $valarr = array();
574 $neworig = array();
575 $pairs = array();
576 $i = -1;
577 $cnt = 0;
578 foreach($table->flds as $name=>$fld) {
579 $i += 1;
580 $val = $this->$name;
581 $neworig[] = $val;
583 if (isset($table->keys[$name])) {
584 continue;
587 if (is_null($val)) {
588 if (isset($fld->not_null) && $fld->not_null) {
589 if (isset($fld->default_value) && strlen($fld->default_value)) continue;
590 else {
591 $this->Error("Cannot set field $name to NULL","Update");
592 return false;
597 if (isset($this->_original[$i]) && $val == $this->_original[$i]) {
598 continue;
600 $valarr[] = $val;
601 $pairs[] = $name.'='.$db->Param($cnt);
602 $cnt += 1;
606 if (!$cnt) return -1;
607 $sql = 'UPDATE '.$this->_table." SET ".implode(",",$pairs)." WHERE ".$where;
608 $ok = $db->Execute($sql,$valarr);
609 if ($ok) {
610 $this->_original =& $neworig;
611 return 1;
613 return 0;
616 function GetAttributeNames()
618 $table =& $this->TableInfo();
619 if (!$table) return false;
620 return array_keys($table->flds);