Browse Source

HHVM Support

nareshv 11 years ago
parent
commit
75bdeb049a
100 changed files with 15897 additions and 0 deletions
  1. 48 0
      hhvm/README.md
  2. 0 0
      hhvm/__init__.py
  3. 1362 0
      hhvm/apc.php
  4. 47 0
      hhvm/benchmark_config
  5. 47 0
      hhvm/dborm.php
  6. 38 0
      hhvm/dbraw.php
  7. 29 0
      hhvm/deploy/config.hdf
  8. 43 0
      hhvm/fortune.php
  9. 17 0
      hhvm/json.php
  10. 6 0
      hhvm/models/World.php
  11. 6 0
      hhvm/php-activerecord/.gitignore
  12. 49 0
      hhvm/php-activerecord/ActiveRecord.php
  13. 25 0
      hhvm/php-activerecord/CHANGELOG
  14. 23 0
      hhvm/php-activerecord/LICENSE
  15. 149 0
      hhvm/php-activerecord/README.md
  16. 16 0
      hhvm/php-activerecord/composer.json
  17. 37 0
      hhvm/php-activerecord/examples/orders/models/Order.php
  18. 9 0
      hhvm/php-activerecord/examples/orders/models/Payment.php
  19. 13 0
      hhvm/php-activerecord/examples/orders/models/Person.php
  20. 79 0
      hhvm/php-activerecord/examples/orders/orders.php
  21. 29 0
      hhvm/php-activerecord/examples/orders/orders.sql
  22. 17 0
      hhvm/php-activerecord/examples/simple/simple.php
  23. 7 0
      hhvm/php-activerecord/examples/simple/simple.sql
  24. 32 0
      hhvm/php-activerecord/examples/simple/simple_with_options.php
  25. 6 0
      hhvm/php-activerecord/examples/simple/simple_with_options.sql
  26. 80 0
      hhvm/php-activerecord/lib/Cache.php
  27. 252 0
      hhvm/php-activerecord/lib/CallBack.php
  28. 155 0
      hhvm/php-activerecord/lib/Column.php
  29. 304 0
      hhvm/php-activerecord/lib/Config.php
  30. 523 0
      hhvm/php-activerecord/lib/Connection.php
  31. 51 0
      hhvm/php-activerecord/lib/ConnectionManager.php
  32. 151 0
      hhvm/php-activerecord/lib/DateTime.php
  33. 137 0
      hhvm/php-activerecord/lib/Exceptions.php
  34. 185 0
      hhvm/php-activerecord/lib/Expressions.php
  35. 120 0
      hhvm/php-activerecord/lib/Inflector.php
  36. 1861 0
      hhvm/php-activerecord/lib/Model.php
  37. 86 0
      hhvm/php-activerecord/lib/Reflections.php
  38. 685 0
      hhvm/php-activerecord/lib/Relationship.php
  39. 423 0
      hhvm/php-activerecord/lib/SQLBuilder.php
  40. 372 0
      hhvm/php-activerecord/lib/Serialization.php
  41. 57 0
      hhvm/php-activerecord/lib/Singleton.php
  42. 555 0
      hhvm/php-activerecord/lib/Table.php
  43. 359 0
      hhvm/php-activerecord/lib/Utils.php
  44. 912 0
      hhvm/php-activerecord/lib/Validations.php
  45. 99 0
      hhvm/php-activerecord/lib/adapters/MysqlAdapter.php
  46. 146 0
      hhvm/php-activerecord/lib/adapters/OciAdapter.php
  47. 139 0
      hhvm/php-activerecord/lib/adapters/PgsqlAdapter.php
  48. 110 0
      hhvm/php-activerecord/lib/adapters/SqliteAdapter.php
  49. 45 0
      hhvm/php-activerecord/lib/cache/Memcache.php
  50. 46 0
      hhvm/php-activerecord/test/ActiveRecordCacheTest.php
  51. 464 0
      hhvm/php-activerecord/test/ActiveRecordFindTest.php
  52. 523 0
      hhvm/php-activerecord/test/ActiveRecordTest.php
  53. 428 0
      hhvm/php-activerecord/test/ActiveRecordWriteTest.php
  54. 24 0
      hhvm/php-activerecord/test/AllTests.php
  55. 21 0
      hhvm/php-activerecord/test/AllValidationsTest.php
  56. 84 0
      hhvm/php-activerecord/test/CacheTest.php
  57. 282 0
      hhvm/php-activerecord/test/CallbackTest.php
  58. 122 0
      hhvm/php-activerecord/test/ColumnTest.php
  59. 96 0
      hhvm/php-activerecord/test/ConfigTest.php
  60. 40 0
      hhvm/php-activerecord/test/ConnectionManagerTest.php
  61. 81 0
      hhvm/php-activerecord/test/ConnectionTest.php
  62. 19 0
      hhvm/php-activerecord/test/DateFormatTest.php
  63. 128 0
      hhvm/php-activerecord/test/DateTimeTest.php
  64. 202 0
      hhvm/php-activerecord/test/ExpressionsTest.php
  65. 61 0
      hhvm/php-activerecord/test/HasManyThroughTest.php
  66. 29 0
      hhvm/php-activerecord/test/InflectorTest.php
  67. 98 0
      hhvm/php-activerecord/test/ModelCallbackTest.php
  68. 38 0
      hhvm/php-activerecord/test/MysqlAdapterTest.php
  69. 47 0
      hhvm/php-activerecord/test/OciAdapterTest.php
  70. 45 0
      hhvm/php-activerecord/test/PgsqlAdapterTest.php
  71. 702 0
      hhvm/php-activerecord/test/RelationshipTest.php
  72. 283 0
      hhvm/php-activerecord/test/SQLBuilderTest.php
  73. 216 0
      hhvm/php-activerecord/test/SerializationTest.php
  74. 64 0
      hhvm/php-activerecord/test/SqliteAdapterTest.php
  75. 108 0
      hhvm/php-activerecord/test/UtilsTest.php
  76. 112 0
      hhvm/php-activerecord/test/ValidatesFormatOfTest.php
  77. 158 0
      hhvm/php-activerecord/test/ValidatesInclusionAndExclusionOfTest.php
  78. 336 0
      hhvm/php-activerecord/test/ValidatesLengthOfTest.php
  79. 166 0
      hhvm/php-activerecord/test/ValidatesNumericalityOfTest.php
  80. 75 0
      hhvm/php-activerecord/test/ValidatesPresenceOfTest.php
  81. 183 0
      hhvm/php-activerecord/test/ValidationsTest.php
  82. 4 0
      hhvm/php-activerecord/test/fixtures/amenities.csv
  83. 5 0
      hhvm/php-activerecord/test/fixtures/authors.csv
  84. 4 0
      hhvm/php-activerecord/test/fixtures/awesome_people.csv
  85. 3 0
      hhvm/php-activerecord/test/fixtures/books.csv
  86. 4 0
      hhvm/php-activerecord/test/fixtures/employees.csv
  87. 7 0
      hhvm/php-activerecord/test/fixtures/events.csv
  88. 5 0
      hhvm/php-activerecord/test/fixtures/hosts.csv
  89. 2 0
      hhvm/php-activerecord/test/fixtures/newsletters.csv
  90. 4 0
      hhvm/php-activerecord/test/fixtures/positions.csv
  91. 3 0
      hhvm/php-activerecord/test/fixtures/property.csv
  92. 5 0
      hhvm/php-activerecord/test/fixtures/property_amenities.csv
  93. 2 0
      hhvm/php-activerecord/test/fixtures/rm-bldg.csv
  94. 2 0
      hhvm/php-activerecord/test/fixtures/user_newsletters.csv
  95. 2 0
      hhvm/php-activerecord/test/fixtures/users.csv
  96. 0 0
      hhvm/php-activerecord/test/fixtures/valuestore.csv
  97. 7 0
      hhvm/php-activerecord/test/fixtures/venues.csv
  98. 405 0
      hhvm/php-activerecord/test/helpers/AdapterTest.php
  99. 130 0
      hhvm/php-activerecord/test/helpers/DatabaseLoader.php
  100. 81 0
      hhvm/php-activerecord/test/helpers/DatabaseTest.php

+ 48 - 0
hhvm/README.md

@@ -0,0 +1,48 @@
+#HHVM Benchmarking Test
+
+This is the [HHVM](http://github.com/facebook/hhvm) portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+### Plaintext Test
+
+* [Plaintext test source](plaintext.php)
+
+### Fortune Test
+
+* [Fortune test source](fortune.php)
+
+
+### JSON Encoding Test
+Use the built-in JSON Encoder
+
+* [JSON test source](json.php)
+
+### Data-Store/Database Mapping Test
+
+* [Database test source Raw](dbraw.php)
+* [Database test source ORM](dborm.php)
+
+## Infrastructure Software Versions
+The tests were run with:
+
+* [HHVM v2.2.0](http://github.com/facebook/hhvm)
+
+## Test URLs
+### JSON Encoding Test
+
+http://localhost/json.php
+
+### Data-Store/Database Mapping Test
+
+Raw:
+http://localhost/dbraw.php
+
+ORM:
+http://localhost/dborm.php
+
+### Variable Query Test
+
+Raw:
+http://localhost/dbraw.php?queries=5
+
+ORM:
+http://localhost/dborm.php?queries=5

+ 0 - 0
hhvm/__init__.py


+ 1362 - 0
hhvm/apc.php

@@ -0,0 +1,1362 @@
+<?php
+/*
+  +----------------------------------------------------------------------+
+  | APC                                                                  |
+  +----------------------------------------------------------------------+
+  | Copyright (c) 2006-2011 The PHP Group                                |
+  +----------------------------------------------------------------------+
+  | This source file is subject to version 3.01 of the PHP license,      |
+  | that is bundled with this package in the file LICENSE, and is        |
+  | available through the world-wide-web at the following url:           |
+  | http://www.php.net/license/3_01.txt                                  |
+  | If you did not receive a copy of the PHP license and are unable to   |
+  | obtain it through the world-wide-web, please send a note to          |
+  | [email protected] so we can mail you a copy immediately.               |
+  +----------------------------------------------------------------------+
+  | Authors: Ralf Becker <[email protected]>                               |
+  |          Rasmus Lerdorf <[email protected]>                             |
+  |          Ilia Alshanetsky <[email protected]>                         |
+  +----------------------------------------------------------------------+
+
+   All other licensing and usage conditions are those of the PHP Group.
+
+ */
+
+$VERSION='$Id: apc.php 325483 2012-05-01 00:34:04Z rasmus $';
+
+////////// READ OPTIONAL CONFIGURATION FILE ////////////
+if (file_exists("apc.conf.php")) include("apc.conf.php");
+////////////////////////////////////////////////////////
+
+////////// BEGIN OF DEFAULT CONFIG AREA ///////////////////////////////////////////////////////////
+
+defaults('USE_AUTHENTICATION',1);			// Use (internal) authentication - best choice if 
+											// no other authentication is available
+											// If set to 0:
+											//  There will be no further authentication. You 
+											//  will have to handle this by yourself!
+											// If set to 1:
+											//  You need to change ADMIN_PASSWORD to make
+											//  this work!
+defaults('ADMIN_USERNAME','apc'); 			// Admin Username
+defaults('ADMIN_PASSWORD','password');  	// Admin Password - CHANGE THIS TO ENABLE!!!
+
+// (beckerr) I'm using a clear text password here, because I've no good idea how to let 
+//           users generate a md5 or crypt password in a easy way to fill it in above
+
+//defaults('DATE_FORMAT', "d.m.Y H:i:s");	// German
+defaults('DATE_FORMAT', 'Y/m/d H:i:s'); 	// US
+
+defaults('GRAPH_SIZE',200);					// Image size
+
+//defaults('PROXY', 'tcp://127.0.0.1:8080');
+
+////////// END OF DEFAULT CONFIG AREA /////////////////////////////////////////////////////////////
+
+
+// "define if not defined"
+function defaults($d,$v) {
+	if (!defined($d)) define($d,$v); // or just @define(...)
+}
+
+// rewrite $PHP_SELF to block XSS attacks
+//
+$PHP_SELF= isset($_SERVER['PHP_SELF']) ? htmlentities(strip_tags($_SERVER['PHP_SELF'],''), ENT_QUOTES, 'UTF-8') : '';
+$time = time();
+$host = php_uname('n');
+if($host) { $host = '('.$host.')'; }
+if (isset($_SERVER['SERVER_ADDR'])) {
+  $host .= ' ('.$_SERVER['SERVER_ADDR'].')';
+}
+
+// operation constants
+define('OB_HOST_STATS',1);
+define('OB_SYS_CACHE',2);
+define('OB_USER_CACHE',3);
+define('OB_SYS_CACHE_DIR',4);
+define('OB_VERSION_CHECK',9);
+
+// check validity of input variables
+$vardom=array(
+	'OB'	=> '/^\d+$/',			// operational mode switch
+	'CC'	=> '/^[01]$/',			// clear cache requested
+	'DU'	=> '/^.*$/',			// Delete User Key
+	'SH'	=> '/^[a-z0-9]+$/',		// shared object description
+
+	'IMG'	=> '/^[123]$/',			// image to generate
+	'LO'	=> '/^1$/',				// login requested
+
+	'COUNT'	=> '/^\d+$/',			// number of line displayed in list
+	'SCOPE'	=> '/^[AD]$/',			// list view scope
+	'SORT1'	=> '/^[AHSMCDTZ]$/',	// first sort key
+	'SORT2'	=> '/^[DA]$/',			// second sort key
+	'AGGR'	=> '/^\d+$/',			// aggregation by dir level
+	'SEARCH'	=> '~^[a-zA-Z0-9/_.-]*$~'			// aggregation by dir level
+);
+
+// default cache mode
+$cache_mode='opcode';
+
+// cache scope
+$scope_list=array(
+	'A' => 'cache_list',
+	'D' => 'deleted_list'
+);
+
+// handle POST and GET requests
+if (empty($_REQUEST)) {
+	if (!empty($_GET) && !empty($_POST)) {
+		$_REQUEST = array_merge($_GET, $_POST);
+	} else if (!empty($_GET)) {
+		$_REQUEST = $_GET;
+	} else if (!empty($_POST)) {
+		$_REQUEST = $_POST;
+	} else {
+		$_REQUEST = array();
+	}
+}
+
+// check parameter syntax
+foreach($vardom as $var => $dom) {
+	if (!isset($_REQUEST[$var])) {
+		$MYREQUEST[$var]=NULL;
+	} else if (!is_array($_REQUEST[$var]) && preg_match($dom.'D',$_REQUEST[$var])) {
+		$MYREQUEST[$var]=$_REQUEST[$var];
+	} else {
+		$MYREQUEST[$var]=$_REQUEST[$var]=NULL;
+	}
+}
+
+// check parameter sematics
+if (empty($MYREQUEST['SCOPE'])) $MYREQUEST['SCOPE']="A";
+if (empty($MYREQUEST['SORT1'])) $MYREQUEST['SORT1']="H";
+if (empty($MYREQUEST['SORT2'])) $MYREQUEST['SORT2']="D";
+if (empty($MYREQUEST['OB']))	$MYREQUEST['OB']=OB_HOST_STATS;
+if (!isset($MYREQUEST['COUNT'])) $MYREQUEST['COUNT']=20;
+if (!isset($scope_list[$MYREQUEST['SCOPE']])) $MYREQUEST['SCOPE']='A';
+
+$MY_SELF=
+	"$PHP_SELF".
+	"?SCOPE=".$MYREQUEST['SCOPE'].
+	"&SORT1=".$MYREQUEST['SORT1'].
+	"&SORT2=".$MYREQUEST['SORT2'].
+	"&COUNT=".$MYREQUEST['COUNT'];
+$MY_SELF_WO_SORT=
+	"$PHP_SELF".
+	"?SCOPE=".$MYREQUEST['SCOPE'].
+	"&COUNT=".$MYREQUEST['COUNT'];
+
+// authentication needed?
+//
+if (!USE_AUTHENTICATION) {
+	$AUTHENTICATED=1;
+} else {
+	$AUTHENTICATED=0;
+	if (ADMIN_PASSWORD!='password' && ($MYREQUEST['LO'] == 1 || isset($_SERVER['PHP_AUTH_USER']))) {
+
+		if (!isset($_SERVER['PHP_AUTH_USER']) ||
+			!isset($_SERVER['PHP_AUTH_PW']) ||
+			$_SERVER['PHP_AUTH_USER'] != ADMIN_USERNAME ||
+			$_SERVER['PHP_AUTH_PW'] != ADMIN_PASSWORD) {
+			Header("WWW-Authenticate: Basic realm=\"APC Login\"");
+			Header("HTTP/1.0 401 Unauthorized");
+
+			echo <<<EOB
+				<html><body>
+				<h1>Rejected!</h1>
+				<big>Wrong Username or Password!</big><br/>&nbsp;<br/>&nbsp;
+				<big><a href='$PHP_SELF?OB={$MYREQUEST['OB']}'>Continue...</a></big>
+				</body></html>
+EOB;
+			exit;
+			
+		} else {
+			$AUTHENTICATED=1;
+		}
+	}
+}
+	
+// select cache mode
+if ($AUTHENTICATED && $MYREQUEST['OB'] == OB_USER_CACHE) {
+	$cache_mode='user';
+}
+// clear cache
+if ($AUTHENTICATED && isset($MYREQUEST['CC']) && $MYREQUEST['CC']) {
+	apc_clear_cache($cache_mode);
+}
+
+if ($AUTHENTICATED && !empty($MYREQUEST['DU'])) {
+	apc_delete($MYREQUEST['DU']);
+}
+
+if(!function_exists('apc_cache_info') || !($cache=@apc_cache_info($cache_mode))) {
+	echo "No cache info available.  APC does not appear to be running.";
+  exit;
+}
+
+$cache_user = apc_cache_info('user', 1);  
+$mem=apc_sma_info();
+if(!$cache['num_hits']) { $cache['num_hits']=1; $time++; }  // Avoid division by 0 errors on a cache clear
+
+// don't cache this page
+//
+header("Cache-Control: no-store, no-cache, must-revalidate");  // HTTP/1.1
+header("Cache-Control: post-check=0, pre-check=0", false);
+header("Pragma: no-cache");                                    // HTTP/1.0
+
+function duration($ts) {
+    global $time;
+    $years = (int)((($time - $ts)/(7*86400))/52.177457);
+    $rem = (int)(($time-$ts)-($years * 52.177457 * 7 * 86400));
+    $weeks = (int)(($rem)/(7*86400));
+    $days = (int)(($rem)/86400) - $weeks*7;
+    $hours = (int)(($rem)/3600) - $days*24 - $weeks*7*24;
+    $mins = (int)(($rem)/60) - $hours*60 - $days*24*60 - $weeks*7*24*60;
+    $str = '';
+    if($years==1) $str .= "$years year, ";
+    if($years>1) $str .= "$years years, ";
+    if($weeks==1) $str .= "$weeks week, ";
+    if($weeks>1) $str .= "$weeks weeks, ";
+    if($days==1) $str .= "$days day,";
+    if($days>1) $str .= "$days days,";
+    if($hours == 1) $str .= " $hours hour and";
+    if($hours>1) $str .= " $hours hours and";
+    if($mins == 1) $str .= " 1 minute";
+    else $str .= " $mins minutes";
+    return $str;
+}
+
+// create graphics
+//
+function graphics_avail() {
+	return extension_loaded('gd');
+}
+if (isset($MYREQUEST['IMG']))
+{
+	if (!graphics_avail()) {
+		exit(0);
+	}
+
+	function fill_arc($im, $centerX, $centerY, $diameter, $start, $end, $color1,$color2,$text='',$placeindex=0) {
+		$r=$diameter/2;
+		$w=deg2rad((360+$start+($end-$start)/2)%360);
+
+		
+		if (function_exists("imagefilledarc")) {
+			// exists only if GD 2.0.1 is avaliable
+			imagefilledarc($im, $centerX+1, $centerY+1, $diameter, $diameter, $start, $end, $color1, IMG_ARC_PIE);
+			imagefilledarc($im, $centerX, $centerY, $diameter, $diameter, $start, $end, $color2, IMG_ARC_PIE);
+			imagefilledarc($im, $centerX, $centerY, $diameter, $diameter, $start, $end, $color1, IMG_ARC_NOFILL|IMG_ARC_EDGED);
+		} else {
+			imagearc($im, $centerX, $centerY, $diameter, $diameter, $start, $end, $color2);
+			imageline($im, $centerX, $centerY, $centerX + cos(deg2rad($start)) * $r, $centerY + sin(deg2rad($start)) * $r, $color2);
+			imageline($im, $centerX, $centerY, $centerX + cos(deg2rad($start+1)) * $r, $centerY + sin(deg2rad($start)) * $r, $color2);
+			imageline($im, $centerX, $centerY, $centerX + cos(deg2rad($end-1))   * $r, $centerY + sin(deg2rad($end))   * $r, $color2);
+			imageline($im, $centerX, $centerY, $centerX + cos(deg2rad($end))   * $r, $centerY + sin(deg2rad($end))   * $r, $color2);
+			imagefill($im,$centerX + $r*cos($w)/2, $centerY + $r*sin($w)/2, $color2);
+		}
+		if ($text) {
+			if ($placeindex>0) {
+				imageline($im,$centerX + $r*cos($w)/2, $centerY + $r*sin($w)/2,$diameter, $placeindex*12,$color1);
+				imagestring($im,4,$diameter, $placeindex*12,$text,$color1);	
+				
+			} else {
+				imagestring($im,4,$centerX + $r*cos($w)/2, $centerY + $r*sin($w)/2,$text,$color1);
+			}
+		}
+	} 
+
+	function text_arc($im, $centerX, $centerY, $diameter, $start, $end, $color1,$text,$placeindex=0) {
+		$r=$diameter/2;
+		$w=deg2rad((360+$start+($end-$start)/2)%360);
+
+		if ($placeindex>0) {
+			imageline($im,$centerX + $r*cos($w)/2, $centerY + $r*sin($w)/2,$diameter, $placeindex*12,$color1);
+			imagestring($im,4,$diameter, $placeindex*12,$text,$color1);	
+				
+		} else {
+			imagestring($im,4,$centerX + $r*cos($w)/2, $centerY + $r*sin($w)/2,$text,$color1);
+		}
+	} 
+	
+	function fill_box($im, $x, $y, $w, $h, $color1, $color2,$text='',$placeindex='') {
+		global $col_black;
+		$x1=$x+$w-1;
+		$y1=$y+$h-1;
+
+		imagerectangle($im, $x, $y1, $x1+1, $y+1, $col_black);
+		if($y1>$y) imagefilledrectangle($im, $x, $y, $x1, $y1, $color2);
+		else imagefilledrectangle($im, $x, $y1, $x1, $y, $color2);
+		imagerectangle($im, $x, $y1, $x1, $y, $color1);
+		if ($text) {
+			if ($placeindex>0) {
+			
+				if ($placeindex<16)
+				{
+					$px=5;
+					$py=$placeindex*12+6;
+					imagefilledrectangle($im, $px+90, $py+3, $px+90-4, $py-3, $color2);
+					imageline($im,$x,$y+$h/2,$px+90,$py,$color2);
+					imagestring($im,2,$px,$py-6,$text,$color1);	
+					
+				} else {
+					if ($placeindex<31) {
+						$px=$x+40*2;
+						$py=($placeindex-15)*12+6;
+					} else {
+						$px=$x+40*2+100*intval(($placeindex-15)/15);
+						$py=($placeindex%15)*12+6;
+					}
+					imagefilledrectangle($im, $px, $py+3, $px-4, $py-3, $color2);
+					imageline($im,$x+$w,$y+$h/2,$px,$py,$color2);
+					imagestring($im,2,$px+2,$py-6,$text,$color1);	
+				}
+			} else {
+				imagestring($im,4,$x+5,$y1-16,$text,$color1);
+			}
+		}
+	}
+
+
+	$size = GRAPH_SIZE; // image size
+	if ($MYREQUEST['IMG']==3)
+		$image = imagecreate(2*$size+150, $size+10);
+	else
+		$image = imagecreate($size+50, $size+10);
+
+	$col_white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
+	$col_red   = imagecolorallocate($image, 0xD0, 0x60,  0x30);
+	$col_green = imagecolorallocate($image, 0x60, 0xF0, 0x60);
+	$col_black = imagecolorallocate($image,   0,   0,   0);
+	imagecolortransparent($image,$col_white);
+
+	switch ($MYREQUEST['IMG']) {
+	
+	case 1:
+		$s=$mem['num_seg']*$mem['seg_size'];
+		$a=$mem['avail_mem'];
+		$x=$y=$size/2;
+		$fuzz = 0.000001;
+
+		// This block of code creates the pie chart.  It is a lot more complex than you
+		// would expect because we try to visualize any memory fragmentation as well.
+		$angle_from = 0;
+		$string_placement=array();
+		for($i=0; $i<$mem['num_seg']; $i++) {	
+			$ptr = 0;
+			$free = $mem['block_lists'][$i];
+			uasort($free, 'block_sort');
+			foreach($free as $block) {
+				if($block['offset']!=$ptr) {       // Used block
+					$angle_to = $angle_from+($block['offset']-$ptr)/$s;
+					if(($angle_to+$fuzz)>1) $angle_to = 1;
+					if( ($angle_to*360) - ($angle_from*360) >= 1) {
+						fill_arc($image,$x,$y,$size,$angle_from*360,$angle_to*360,$col_black,$col_red);
+						if (($angle_to-$angle_from)>0.05) {
+							array_push($string_placement, array($angle_from,$angle_to));
+						}
+					}
+					$angle_from = $angle_to;
+				}
+				$angle_to = $angle_from+($block['size'])/$s;
+				if(($angle_to+$fuzz)>1) $angle_to = 1;
+				if( ($angle_to*360) - ($angle_from*360) >= 1) {
+					fill_arc($image,$x,$y,$size,$angle_from*360,$angle_to*360,$col_black,$col_green);
+					if (($angle_to-$angle_from)>0.05) {
+						array_push($string_placement, array($angle_from,$angle_to));
+					}
+				}
+				$angle_from = $angle_to;
+				$ptr = $block['offset']+$block['size'];
+			}
+			if ($ptr < $mem['seg_size']) { // memory at the end 
+				$angle_to = $angle_from + ($mem['seg_size'] - $ptr)/$s;
+				if(($angle_to+$fuzz)>1) $angle_to = 1;
+				fill_arc($image,$x,$y,$size,$angle_from*360,$angle_to*360,$col_black,$col_red);
+				if (($angle_to-$angle_from)>0.05) {
+					array_push($string_placement, array($angle_from,$angle_to));
+				}
+			}
+		}
+		foreach ($string_placement as $angle) {
+			text_arc($image,$x,$y,$size,$angle[0]*360,$angle[1]*360,$col_black,bsize($s*($angle[1]-$angle[0])));
+		}
+		break;
+		
+	case 2: 
+		$s=$cache['num_hits']+$cache['num_misses'];
+		$a=$cache['num_hits'];
+		
+		fill_box($image, 30,$size,50,-$a*($size-21)/$s,$col_black,$col_green,sprintf("%.1f%%",$cache['num_hits']*100/$s));
+		fill_box($image,130,$size,50,-max(4,($s-$a)*($size-21)/$s),$col_black,$col_red,sprintf("%.1f%%",$cache['num_misses']*100/$s));
+		break;
+		
+	case 3:
+		$s=$mem['num_seg']*$mem['seg_size'];
+		$a=$mem['avail_mem'];
+		$x=130;
+		$y=1;
+		$j=1;
+
+		// This block of code creates the bar chart.  It is a lot more complex than you
+		// would expect because we try to visualize any memory fragmentation as well.
+		for($i=0; $i<$mem['num_seg']; $i++) {	
+			$ptr = 0;
+			$free = $mem['block_lists'][$i];
+			uasort($free, 'block_sort');
+			foreach($free as $block) {
+				if($block['offset']!=$ptr) {       // Used block
+					$h=(GRAPH_SIZE-5)*($block['offset']-$ptr)/$s;
+					if ($h>0) {
+                                                $j++;
+						if($j<75) fill_box($image,$x,$y,50,$h,$col_black,$col_red,bsize($block['offset']-$ptr),$j);
+                                                else fill_box($image,$x,$y,50,$h,$col_black,$col_red);
+                                        }
+					$y+=$h;
+				}
+				$h=(GRAPH_SIZE-5)*($block['size'])/$s;
+				if ($h>0) {
+                                        $j++;
+					if($j<75) fill_box($image,$x,$y,50,$h,$col_black,$col_green,bsize($block['size']),$j);
+					else fill_box($image,$x,$y,50,$h,$col_black,$col_green);
+                                }
+				$y+=$h;
+				$ptr = $block['offset']+$block['size'];
+			}
+			if ($ptr < $mem['seg_size']) { // memory at the end 
+				$h = (GRAPH_SIZE-5) * ($mem['seg_size'] - $ptr) / $s;
+				if ($h > 0) {
+					fill_box($image,$x,$y,50,$h,$col_black,$col_red,bsize($mem['seg_size']-$ptr),$j++);
+				}
+			}
+		}
+		break;
+	case 4: 
+		$s=$cache['num_hits']+$cache['num_misses'];
+		$a=$cache['num_hits'];
+	        	
+		fill_box($image, 30,$size,50,-$a*($size-21)/$s,$col_black,$col_green,sprintf("%.1f%%",$cache['num_hits']*100/$s));
+		fill_box($image,130,$size,50,-max(4,($s-$a)*($size-21)/$s),$col_black,$col_red,sprintf("%.1f%%",$cache['num_misses']*100/$s));
+		break;
+	
+	}
+	header("Content-type: image/png");
+	imagepng($image);
+	exit;
+}
+
+// pretty printer for byte values
+//
+function bsize($s) {
+	foreach (array('','K','M','G') as $i => $k) {
+		if ($s < 1024) break;
+		$s/=1024;
+	}
+	return sprintf("%5.1f %sBytes",$s,$k);
+}
+
+// sortable table header in "scripts for this host" view
+function sortheader($key,$name,$extra='') {
+	global $MYREQUEST, $MY_SELF_WO_SORT;
+	
+	if ($MYREQUEST['SORT1']==$key) {
+		$MYREQUEST['SORT2'] = $MYREQUEST['SORT2']=='A' ? 'D' : 'A';
+	}
+	return "<a class=sortable href=\"$MY_SELF_WO_SORT$extra&SORT1=$key&SORT2=".$MYREQUEST['SORT2']."\">$name</a>";
+
+}
+
+// create menu entry 
+function menu_entry($ob,$title) {
+	global $MYREQUEST,$MY_SELF;
+	if ($MYREQUEST['OB']!=$ob) {
+		return "<li><a href=\"$MY_SELF&OB=$ob\">$title</a></li>";
+	} else if (empty($MYREQUEST['SH'])) {
+		return "<li><span class=active>$title</span></li>";
+	} else {
+		return "<li><a class=\"child_active\" href=\"$MY_SELF&OB=$ob\">$title</a></li>";	
+	}
+}
+
+function put_login_link($s="Login")
+{
+	global $MY_SELF,$MYREQUEST,$AUTHENTICATED;
+	// needs ADMIN_PASSWORD to be changed!
+	//
+	if (!USE_AUTHENTICATION) {
+		return;
+	} else if (ADMIN_PASSWORD=='password')
+	{
+		print <<<EOB
+			<a href="#" onClick="javascript:alert('You need to set a password at the top of apc.php before this will work!');return false";>$s</a>
+EOB;
+	} else if ($AUTHENTICATED) {
+		print <<<EOB
+			'{$_SERVER['PHP_AUTH_USER']}'&nbsp;logged&nbsp;in!
+EOB;
+	} else{
+		print <<<EOB
+			<a href="$MY_SELF&LO=1&OB={$MYREQUEST['OB']}">$s</a>
+EOB;
+	}
+}
+
+function block_sort($array1, $array2)
+{
+	if ($array1['offset'] > $array2['offset']) {
+		return 1;
+	} else {
+		return -1;
+	}
+}
+
+
+?>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head><title>APC INFO <?php echo $host ?></title>
+<style><!--
+body { background:white; font-size:100.01%; margin:0; padding:0; }
+body,p,td,th,input,submit { font-size:0.8em;font-family:arial,helvetica,sans-serif; }
+* html body   {font-size:0.8em}
+* html p      {font-size:0.8em}
+* html td     {font-size:0.8em}
+* html th     {font-size:0.8em}
+* html input  {font-size:0.8em}
+* html submit {font-size:0.8em}
+td { vertical-align:top }
+a { color:black; font-weight:none; text-decoration:none; }
+a:hover { text-decoration:underline; }
+div.content { padding:1em 1em 1em 1em; position:absolute; width:97%; z-index:100; }
+
+
+div.head div.login {
+	position:absolute;
+	right: 1em;
+	top: 1.2em;
+	color:white;
+	width:6em;
+	}
+div.head div.login a {
+	position:absolute;
+	right: 0em;
+	background:rgb(119,123,180);
+	border:solid rgb(102,102,153) 2px;
+	color:white;
+	font-weight:bold;
+	padding:0.1em 0.5em 0.1em 0.5em;
+	text-decoration:none;
+	}
+div.head div.login a:hover {
+	background:rgb(193,193,244);
+	}
+
+h1.apc { background:rgb(153,153,204); margin:0; padding:0.5em 1em 0.5em 1em; }
+* html h1.apc { margin-bottom:-7px; }
+h1.apc a:hover { text-decoration:none; color:rgb(90,90,90); }
+h1.apc div.logo span.logo {
+	background:rgb(119,123,180);
+	color:black;
+	border-right: solid black 1px;
+	border-bottom: solid black 1px;
+	font-style:italic;
+	font-size:1em;
+	padding-left:1.2em;
+	padding-right:1.2em;
+	text-align:right;
+	}
+h1.apc div.logo span.name { color:white; font-size:0.7em; padding:0 0.8em 0 2em; }
+h1.apc div.nameinfo { color:white; display:inline; font-size:0.4em; margin-left: 3em; }
+h1.apc div.copy { color:black; font-size:0.4em; position:absolute; right:1em; }
+hr.apc {
+	background:white;
+	border-bottom:solid rgb(102,102,153) 1px;
+	border-style:none;
+	border-top:solid rgb(102,102,153) 10px;
+	height:12px;
+	margin:0;
+	margin-top:1px;
+	padding:0;
+}
+
+ol,menu { margin:1em 0 0 0; padding:0.2em; margin-left:1em;}
+ol.menu li { display:inline; margin-right:0.7em; list-style:none; font-size:85%}
+ol.menu a {
+	background:rgb(153,153,204);
+	border:solid rgb(102,102,153) 2px;
+	color:white;
+	font-weight:bold;
+	margin-right:0em;
+	padding:0.1em 0.5em 0.1em 0.5em;
+	text-decoration:none;
+	margin-left: 5px;
+	}
+ol.menu a.child_active {
+	background:rgb(153,153,204);
+	border:solid rgb(102,102,153) 2px;
+	color:white;
+	font-weight:bold;
+	margin-right:0em;
+	padding:0.1em 0.5em 0.1em 0.5em;
+	text-decoration:none;
+	border-left: solid black 5px;
+	margin-left: 0px;
+	}
+ol.menu span.active {
+	background:rgb(153,153,204);
+	border:solid rgb(102,102,153) 2px;
+	color:black;
+	font-weight:bold;
+	margin-right:0em;
+	padding:0.1em 0.5em 0.1em 0.5em;
+	text-decoration:none;
+	border-left: solid black 5px;
+	}
+ol.menu span.inactive {
+	background:rgb(193,193,244);
+	border:solid rgb(182,182,233) 2px;
+	color:white;
+	font-weight:bold;
+	margin-right:0em;
+	padding:0.1em 0.5em 0.1em 0.5em;
+	text-decoration:none;
+	margin-left: 5px;
+	}
+ol.menu a:hover {
+	background:rgb(193,193,244);
+	text-decoration:none;
+	}
+	
+	
+div.info {
+	background:rgb(204,204,204);
+	border:solid rgb(204,204,204) 1px;
+	margin-bottom:1em;
+	}
+div.info h2 {
+	background:rgb(204,204,204);
+	color:black;
+	font-size:1em;
+	margin:0;
+	padding:0.1em 1em 0.1em 1em;
+	}
+div.info table {
+	border:solid rgb(204,204,204) 1px;
+	border-spacing:0;
+	width:100%;
+	}
+div.info table th {
+	background:rgb(204,204,204);
+	color:white;
+	margin:0;
+	padding:0.1em 1em 0.1em 1em;
+	}
+div.info table th a.sortable { color:black; }
+div.info table tr.tr-0 { background:rgb(238,238,238); }
+div.info table tr.tr-1 { background:rgb(221,221,221); }
+div.info table td { padding:0.3em 1em 0.3em 1em; }
+div.info table td.td-0 { border-right:solid rgb(102,102,153) 1px; white-space:nowrap; }
+div.info table td.td-n { border-right:solid rgb(102,102,153) 1px; }
+div.info table td h3 {
+	color:black;
+	font-size:1.1em;
+	margin-left:-0.3em;
+	}
+
+div.graph { margin-bottom:1em }
+div.graph h2 { background:rgb(204,204,204);; color:black; font-size:1em; margin:0; padding:0.1em 1em 0.1em 1em; }
+div.graph table { border:solid rgb(204,204,204) 1px; color:black; font-weight:normal; width:100%; }
+div.graph table td.td-0 { background:rgb(238,238,238); }
+div.graph table td.td-1 { background:rgb(221,221,221); }
+div.graph table td { padding:0.2em 1em 0.4em 1em; }
+
+div.div1,div.div2 { margin-bottom:1em; width:35em; }
+div.div3 { position:absolute; left:40em; top:1em; width:580px; }
+//div.div3 { position:absolute; left:37em; top:1em; right:1em; }
+
+div.sorting { margin:1.5em 0em 1.5em 2em }
+.center { text-align:center }
+.aright { position:absolute;right:1em }
+.right { text-align:right }
+.ok { color:rgb(0,200,0); font-weight:bold}
+.failed { color:rgb(200,0,0); font-weight:bold}
+
+span.box {
+	border: black solid 1px;
+	border-right:solid black 2px;
+	border-bottom:solid black 2px;
+	padding:0 0.5em 0 0.5em;
+	margin-right:1em;
+}
+span.green { background:#60F060; padding:0 0.5em 0 0.5em}
+span.red { background:#D06030; padding:0 0.5em 0 0.5em }
+
+div.authneeded {
+	background:rgb(238,238,238);
+	border:solid rgb(204,204,204) 1px;
+	color:rgb(200,0,0);
+	font-size:1.2em;
+	font-weight:bold;
+	padding:2em;
+	text-align:center;
+	}
+	
+input {
+	background:rgb(153,153,204);
+	border:solid rgb(102,102,153) 2px;
+	color:white;
+	font-weight:bold;
+	margin-right:1em;
+	padding:0.1em 0.5em 0.1em 0.5em;
+	}
+//-->
+</style>
+</head>
+<body>
+<div class="head">
+	<h1 class="apc">
+		<div class="logo"><span class="logo"><a href="http://pecl.php.net/package/APC">APC</a></span></div>
+		<div class="nameinfo">Opcode Cache</div>
+	</h1>
+	<div class="login">
+	<?php put_login_link(); ?>
+	</div>
+	<hr class="apc">
+</div>
+<?php
+
+
+// Display main Menu
+echo <<<EOB
+	<ol class=menu>
+	<li><a href="$MY_SELF&OB={$MYREQUEST['OB']}&SH={$MYREQUEST['SH']}">Refresh Data</a></li>
+EOB;
+echo
+	menu_entry(1,'View Host Stats'),
+	menu_entry(2,'System Cache Entries');
+if ($AUTHENTICATED) {
+	echo menu_entry(4,'Per-Directory Entries');
+}
+echo
+	menu_entry(3,'User Cache Entries'),
+	menu_entry(9,'Version Check');
+	
+if ($AUTHENTICATED) {
+	echo <<<EOB
+		<li><a class="aright" href="$MY_SELF&CC=1&OB={$MYREQUEST['OB']}" onClick="javascript:return confirm('Are you sure?');">Clear $cache_mode Cache</a></li>
+EOB;
+}
+echo <<<EOB
+	</ol>
+EOB;
+
+
+// CONTENT
+echo <<<EOB
+	<div class=content>
+EOB;
+
+// MAIN SWITCH STATEMENT 
+
+switch ($MYREQUEST['OB']) {
+
+
+
+
+
+// -----------------------------------------------
+// Host Stats
+// -----------------------------------------------
+case OB_HOST_STATS:
+	$mem_size = $mem['num_seg']*$mem['seg_size'];
+	$mem_avail= $mem['avail_mem'];
+	$mem_used = $mem_size-$mem_avail;
+	$seg_size = bsize($mem['seg_size']);
+	$req_rate = sprintf("%.2f",($cache['num_hits']+$cache['num_misses'])/($time-$cache['start_time']));
+	$hit_rate = sprintf("%.2f",($cache['num_hits'])/($time-$cache['start_time']));
+	$miss_rate = sprintf("%.2f",($cache['num_misses'])/($time-$cache['start_time']));
+	$insert_rate = sprintf("%.2f",($cache['num_inserts'])/($time-$cache['start_time']));
+	$req_rate_user = sprintf("%.2f",($cache_user['num_hits']+$cache_user['num_misses'])/($time-$cache_user['start_time']));
+	$hit_rate_user = sprintf("%.2f",($cache_user['num_hits'])/($time-$cache_user['start_time']));
+	$miss_rate_user = sprintf("%.2f",($cache_user['num_misses'])/($time-$cache_user['start_time']));
+	$insert_rate_user = sprintf("%.2f",($cache_user['num_inserts'])/($time-$cache_user['start_time']));
+	$apcversion = phpversion('apc');
+	$phpversion = phpversion();
+	$number_files = $cache['num_entries']; 
+    $size_files = bsize($cache['mem_size']);
+	$number_vars = $cache_user['num_entries'];
+    $size_vars = bsize($cache_user['mem_size']);
+	$i=0;
+	echo <<< EOB
+		<div class="info div1"><h2>General Cache Information</h2>
+		<table cellspacing=0><tbody>
+		<tr class=tr-0><td class=td-0>APC Version</td><td>$apcversion</td></tr>
+		<tr class=tr-1><td class=td-0>PHP Version</td><td>$phpversion</td></tr>
+EOB;
+
+	if(!empty($_SERVER['SERVER_NAME']))
+		echo "<tr class=tr-0><td class=td-0>APC Host</td><td>{$_SERVER['SERVER_NAME']} $host</td></tr>\n";
+	if(!empty($_SERVER['SERVER_SOFTWARE']))
+		echo "<tr class=tr-1><td class=td-0>Server Software</td><td>{$_SERVER['SERVER_SOFTWARE']}</td></tr>\n";
+
+	echo <<<EOB
+		<tr class=tr-0><td class=td-0>Shared Memory</td><td>{$mem['num_seg']} Segment(s) with $seg_size 
+    <br/> ({$cache['memory_type']} memory, {$cache['locking_type']} locking)
+    </td></tr>
+EOB;
+	echo   '<tr class=tr-1><td class=td-0>Start Time</td><td>',date(DATE_FORMAT,$cache['start_time']),'</td></tr>';
+	echo   '<tr class=tr-0><td class=td-0>Uptime</td><td>',duration($cache['start_time']),'</td></tr>';
+	echo   '<tr class=tr-1><td class=td-0>File Upload Support</td><td>',$cache['file_upload_progress'],'</td></tr>';
+	echo <<<EOB
+		</tbody></table>
+		</div>
+
+		<div class="info div1"><h2>File Cache Information</h2>
+		<table cellspacing=0><tbody>
+		<tr class=tr-0><td class=td-0>Cached Files</td><td>$number_files ($size_files)</td></tr>
+		<tr class=tr-1><td class=td-0>Hits</td><td>{$cache['num_hits']}</td></tr>
+		<tr class=tr-0><td class=td-0>Misses</td><td>{$cache['num_misses']}</td></tr>
+		<tr class=tr-1><td class=td-0>Request Rate (hits, misses)</td><td>$req_rate cache requests/second</td></tr>
+		<tr class=tr-0><td class=td-0>Hit Rate</td><td>$hit_rate cache requests/second</td></tr>
+		<tr class=tr-1><td class=td-0>Miss Rate</td><td>$miss_rate cache requests/second</td></tr>
+		<tr class=tr-0><td class=td-0>Insert Rate</td><td>$insert_rate cache requests/second</td></tr>
+		<tr class=tr-1><td class=td-0>Cache full count</td><td>{$cache['expunges']}</td></tr>
+		</tbody></table>
+		</div>
+
+		<div class="info div1"><h2>User Cache Information</h2>
+		<table cellspacing=0><tbody>
+    <tr class=tr-0><td class=td-0>Cached Variables</td><td>$number_vars ($size_vars)</td></tr>
+		<tr class=tr-1><td class=td-0>Hits</td><td>{$cache_user['num_hits']}</td></tr>
+		<tr class=tr-0><td class=td-0>Misses</td><td>{$cache_user['num_misses']}</td></tr>
+		<tr class=tr-1><td class=td-0>Request Rate (hits, misses)</td><td>$req_rate_user cache requests/second</td></tr>
+		<tr class=tr-0><td class=td-0>Hit Rate</td><td>$hit_rate_user cache requests/second</td></tr>
+		<tr class=tr-1><td class=td-0>Miss Rate</td><td>$miss_rate_user cache requests/second</td></tr>
+		<tr class=tr-0><td class=td-0>Insert Rate</td><td>$insert_rate_user cache requests/second</td></tr>
+		<tr class=tr-1><td class=td-0>Cache full count</td><td>{$cache_user['expunges']}</td></tr>
+
+		</tbody></table>
+		</div>
+
+		<div class="info div2"><h2>Runtime Settings</h2><table cellspacing=0><tbody>
+EOB;
+
+	$j = 0;
+	foreach (ini_get_all('apc') as $k => $v) {
+		echo "<tr class=tr-$j><td class=td-0>",$k,"</td><td>",str_replace(',',',<br />',$v['local_value']),"</td></tr>\n";
+		$j = 1 - $j;
+	}
+
+	if($mem['num_seg']>1 || $mem['num_seg']==1 && count($mem['block_lists'][0])>1)
+		$mem_note = "Memory Usage<br /><font size=-2>(multiple slices indicate fragments)</font>";
+	else
+		$mem_note = "Memory Usage";
+
+	echo <<< EOB
+		</tbody></table>
+		</div>
+
+		<div class="graph div3"><h2>Host Status Diagrams</h2>
+		<table cellspacing=0><tbody>
+EOB;
+	$size='width='.(GRAPH_SIZE+50).' height='.(GRAPH_SIZE+10);
+	echo <<<EOB
+		<tr>
+		<td class=td-0>$mem_note</td>
+		<td class=td-1>Hits &amp; Misses</td>
+		</tr>
+EOB;
+
+	echo
+		graphics_avail() ? 
+			  '<tr>'.
+			  "<td class=td-0><img alt=\"\" $size src=\"$PHP_SELF?IMG=1&$time\"></td>".
+			  "<td class=td-1><img alt=\"\" $size src=\"$PHP_SELF?IMG=2&$time\"></td></tr>\n"
+			: "",
+		'<tr>',
+		'<td class=td-0><span class="green box">&nbsp;</span>Free: ',bsize($mem_avail).sprintf(" (%.1f%%)",$mem_avail*100/$mem_size),"</td>\n",
+		'<td class=td-1><span class="green box">&nbsp;</span>Hits: ',$cache['num_hits'].sprintf(" (%.1f%%)",$cache['num_hits']*100/($cache['num_hits']+$cache['num_misses'])),"</td>\n",
+		'</tr>',
+		'<tr>',
+		'<td class=td-0><span class="red box">&nbsp;</span>Used: ',bsize($mem_used ).sprintf(" (%.1f%%)",$mem_used *100/$mem_size),"</td>\n",
+		'<td class=td-1><span class="red box">&nbsp;</span>Misses: ',$cache['num_misses'].sprintf(" (%.1f%%)",$cache['num_misses']*100/($cache['num_hits']+$cache['num_misses'])),"</td>\n";
+	echo <<< EOB
+		</tr>
+		</tbody></table>
+
+		<br/>
+		<h2>Detailed Memory Usage and Fragmentation</h2>
+		<table cellspacing=0><tbody>
+		<tr>
+		<td class=td-0 colspan=2><br/>
+EOB;
+
+	// Fragementation: (freeseg - 1) / total_seg
+	$nseg = $freeseg = $fragsize = $freetotal = 0;
+	for($i=0; $i<$mem['num_seg']; $i++) {
+		$ptr = 0;
+		foreach($mem['block_lists'][$i] as $block) {
+			if ($block['offset'] != $ptr) {
+				++$nseg;
+			}
+			$ptr = $block['offset'] + $block['size'];
+                        /* Only consider blocks <5M for the fragmentation % */
+                        if($block['size']<(5*1024*1024)) $fragsize+=$block['size'];
+                        $freetotal+=$block['size'];
+		}
+		$freeseg += count($mem['block_lists'][$i]);
+	}
+	
+	if ($freeseg > 1) {
+		$frag = sprintf("%.2f%% (%s out of %s in %d fragments)", ($fragsize/$freetotal)*100,bsize($fragsize),bsize($freetotal),$freeseg);
+	} else {
+		$frag = "0%";
+	}
+
+	if (graphics_avail()) {
+		$size='width='.(2*GRAPH_SIZE+150).' height='.(GRAPH_SIZE+10);
+		echo <<<EOB
+			<img alt="" $size src="$PHP_SELF?IMG=3&$time">
+EOB;
+	}
+	echo <<<EOB
+		</br>Fragmentation: $frag
+		</td>
+		</tr>
+EOB;
+        if(isset($mem['adist'])) {
+          foreach($mem['adist'] as $i=>$v) {
+            $cur = pow(2,$i); $nxt = pow(2,$i+1)-1;
+            if($i==0) $range = "1";
+            else $range = "$cur - $nxt";
+            echo "<tr><th align=right>$range</th><td align=right>$v</td></tr>\n";
+          }
+        }
+        echo <<<EOB
+		</tbody></table>
+		</div>
+EOB;
+		
+	break;
+
+
+// -----------------------------------------------
+// User Cache Entries
+// -----------------------------------------------
+case OB_USER_CACHE:
+	if (!$AUTHENTICATED) {
+    echo '<div class="error">You need to login to see the user values here!<br/>&nbsp;<br/>';
+		put_login_link("Login now!");
+		echo '</div>';
+		break;
+	}
+	$fieldname='info';
+	$fieldheading='User Entry Label';
+	$fieldkey='info';
+
+// -----------------------------------------------
+// System Cache Entries		
+// -----------------------------------------------
+case OB_SYS_CACHE:	
+	if (!isset($fieldname))
+	{
+		$fieldname='filename';
+		$fieldheading='Script Filename';
+		if(ini_get("apc.stat")) $fieldkey='inode';
+		else $fieldkey='filename'; 
+	}
+	if (!empty($MYREQUEST['SH']))
+	{
+		echo <<< EOB
+			<div class="info"><table cellspacing=0><tbody>
+			<tr><th>Attribute</th><th>Value</th></tr>
+EOB;
+
+		$m=0;
+		foreach($scope_list as $j => $list) {
+			foreach($cache[$list] as $i => $entry) {
+				if (md5($entry[$fieldkey])!=$MYREQUEST['SH']) continue;
+				foreach($entry as $k => $value) {
+					if (!$AUTHENTICATED) {
+						// hide all path entries if not logged in
+						$value=preg_replace('/^.*(\\/|\\\\)/','<i>&lt;hidden&gt;</i>/',$value);
+					}
+
+					if ($k == "num_hits") {
+						$value=sprintf("%s (%.2f%%)",$value,$value*100/$cache['num_hits']);
+					}
+					if ($k == 'deletion_time') {
+						if(!$entry['deletion_time']) $value = "None";
+					}
+					echo
+						"<tr class=tr-$m>",
+						"<td class=td-0>",ucwords(preg_replace("/_/"," ",$k)),"</td>",
+						"<td class=td-last>",(preg_match("/time/",$k) && $value!='None') ? date(DATE_FORMAT,$value) : htmlspecialchars($value, ENT_QUOTES, 'UTF-8'),"</td>",
+						"</tr>";
+					$m=1-$m;
+				}
+				if($fieldkey=='info') {
+					echo "<tr class=tr-$m><td class=td-0>Stored Value</td><td class=td-last><pre>";
+					$output = var_export(apc_fetch($entry[$fieldkey]),true);
+					echo htmlspecialchars($output, ENT_QUOTES, 'UTF-8');
+					echo "</pre></td></tr>\n";
+				}
+				break;
+			}
+		}
+
+		echo <<<EOB
+			</tbody></table>
+			</div>
+EOB;
+		break;
+	}
+
+	$cols=6;
+	echo <<<EOB
+		<div class=sorting><form>Scope:
+		<input type=hidden name=OB value={$MYREQUEST['OB']}>
+		<select name=SCOPE>
+EOB;
+	echo 
+		"<option value=A",$MYREQUEST['SCOPE']=='A' ? " selected":"",">Active</option>",
+		"<option value=D",$MYREQUEST['SCOPE']=='D' ? " selected":"",">Deleted</option>",
+		"</select>",
+		", Sorting:<select name=SORT1>",
+		"<option value=H",$MYREQUEST['SORT1']=='H' ? " selected":"",">Hits</option>",
+		"<option value=Z",$MYREQUEST['SORT1']=='Z' ? " selected":"",">Size</option>",
+		"<option value=S",$MYREQUEST['SORT1']=='S' ? " selected":"",">$fieldheading</option>",
+		"<option value=A",$MYREQUEST['SORT1']=='A' ? " selected":"",">Last accessed</option>",
+		"<option value=M",$MYREQUEST['SORT1']=='M' ? " selected":"",">Last modified</option>",
+		"<option value=C",$MYREQUEST['SORT1']=='C' ? " selected":"",">Created at</option>",
+		"<option value=D",$MYREQUEST['SORT1']=='D' ? " selected":"",">Deleted at</option>";
+	if($fieldname=='info') echo
+		"<option value=D",$MYREQUEST['SORT1']=='T' ? " selected":"",">Timeout</option>";
+	echo 
+		'</select>',
+		'<select name=SORT2>',
+		'<option value=D',$MYREQUEST['SORT2']=='D' ? ' selected':'','>DESC</option>',
+		'<option value=A',$MYREQUEST['SORT2']=='A' ? ' selected':'','>ASC</option>',
+		'</select>',
+		'<select name=COUNT onChange="form.submit()">',
+		'<option value=10 ',$MYREQUEST['COUNT']=='10' ? ' selected':'','>Top 10</option>',
+		'<option value=20 ',$MYREQUEST['COUNT']=='20' ? ' selected':'','>Top 20</option>',
+		'<option value=50 ',$MYREQUEST['COUNT']=='50' ? ' selected':'','>Top 50</option>',
+		'<option value=100',$MYREQUEST['COUNT']=='100'? ' selected':'','>Top 100</option>',
+		'<option value=150',$MYREQUEST['COUNT']=='150'? ' selected':'','>Top 150</option>',
+		'<option value=200',$MYREQUEST['COUNT']=='200'? ' selected':'','>Top 200</option>',
+		'<option value=500',$MYREQUEST['COUNT']=='500'? ' selected':'','>Top 500</option>',
+		'<option value=0  ',$MYREQUEST['COUNT']=='0'  ? ' selected':'','>All</option>',
+		'</select>',
+    '&nbsp; Search: <input name=SEARCH value="',$MYREQUEST['SEARCH'],'" type=text size=25/>',
+		'&nbsp;<input type=submit value="GO!">',
+		'</form></div>';
+
+  if (isset($MYREQUEST['SEARCH'])) {
+   // Don't use preg_quote because we want the user to be able to specify a
+   // regular expression subpattern.
+   $MYREQUEST['SEARCH'] = '/'.str_replace('/', '\\/', $MYREQUEST['SEARCH']).'/i';
+   if (preg_match($MYREQUEST['SEARCH'], 'test') === false) {
+     echo '<div class="error">Error: enter a valid regular expression as a search query.</div>';
+     break;
+   }
+  }
+
+  echo
+		'<div class="info"><table cellspacing=0><tbody>',
+		'<tr>',
+		'<th>',sortheader('S',$fieldheading,  "&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('H','Hits',         "&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('Z','Size',         "&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('A','Last accessed',"&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('M','Last modified',"&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('C','Created at',   "&OB=".$MYREQUEST['OB']),'</th>';
+
+	if($fieldname=='info') {
+		$cols+=2;
+		 echo '<th>',sortheader('T','Timeout',"&OB=".$MYREQUEST['OB']),'</th>';
+	}
+	echo '<th>',sortheader('D','Deleted at',"&OB=".$MYREQUEST['OB']),'</th></tr>';
+
+	// builds list with alpha numeric sortable keys
+	//
+	$list = array();
+	foreach($cache[$scope_list[$MYREQUEST['SCOPE']]] as $i => $entry) {
+		switch($MYREQUEST['SORT1']) {
+			case 'A': $k=sprintf('%015d-',$entry['access_time']); 	break;
+			case 'H': $k=sprintf('%015d-',$entry['num_hits']); 		break;
+			case 'Z': $k=sprintf('%015d-',$entry['mem_size']); 		break;
+			case 'M': $k=sprintf('%015d-',$entry['mtime']);			break;
+			case 'C': $k=sprintf('%015d-',$entry['creation_time']);	break;
+			case 'T': $k=sprintf('%015d-',$entry['ttl']);			break;
+			case 'D': $k=sprintf('%015d-',$entry['deletion_time']);	break;
+			case 'S': $k='';										break;
+		}
+		if (!$AUTHENTICATED) {
+			// hide all path entries if not logged in
+			$list[$k.$entry[$fieldname]]=preg_replace('/^.*(\\/|\\\\)/','*hidden*/',$entry);
+		} else {
+			$list[$k.$entry[$fieldname]]=$entry;
+		}
+	}
+
+	if ($list) {
+		
+		// sort list
+		//
+		switch ($MYREQUEST['SORT2']) {
+			case "A":	krsort($list);	break;
+			case "D":	ksort($list);	break;
+		}
+		
+		// output list
+		$i=0;
+		foreach($list as $k => $entry) {
+      if(!$MYREQUEST['SEARCH'] || preg_match($MYREQUEST['SEARCH'], $entry[$fieldname]) != 0) {  
+        $field_value = htmlentities(strip_tags($entry[$fieldname],''), ENT_QUOTES, 'UTF-8');
+        echo
+          '<tr class=tr-',$i%2,'>',
+          "<td class=td-0><a href=\"$MY_SELF&OB=",$MYREQUEST['OB'],"&SH=",md5($entry[$fieldkey]),"\">",$field_value,'</a></td>',
+          '<td class="td-n center">',$entry['num_hits'],'</td>',
+          '<td class="td-n right">',$entry['mem_size'],'</td>',
+          '<td class="td-n center">',date(DATE_FORMAT,$entry['access_time']),'</td>',
+          '<td class="td-n center">',date(DATE_FORMAT,$entry['mtime']),'</td>',
+          '<td class="td-n center">',date(DATE_FORMAT,$entry['creation_time']),'</td>';
+
+        if($fieldname=='info') {
+          if($entry['ttl'])
+            echo '<td class="td-n center">'.$entry['ttl'].' seconds</td>';
+          else
+            echo '<td class="td-n center">None</td>';
+        }
+        if ($entry['deletion_time']) {
+
+          echo '<td class="td-last center">', date(DATE_FORMAT,$entry['deletion_time']), '</td>';
+        } else if ($MYREQUEST['OB'] == OB_USER_CACHE) {
+
+          echo '<td class="td-last center">';
+          echo '[<a href="', $MY_SELF, '&OB=', $MYREQUEST['OB'], '&DU=', urlencode($entry[$fieldkey]), '">Delete Now</a>]';
+          echo '</td>';
+        } else {
+          echo '<td class="td-last center"> &nbsp; </td>';
+        }
+        echo '</tr>';
+        $i++;
+        if ($i == $MYREQUEST['COUNT'])
+          break;
+      }
+		}
+		
+	} else {
+		echo '<tr class=tr-0><td class="center" colspan=',$cols,'><i>No data</i></td></tr>';
+	}
+	echo <<< EOB
+		</tbody></table>
+EOB;
+
+	if ($list && $i < count($list)) {
+		echo "<a href=\"$MY_SELF&OB=",$MYREQUEST['OB'],"&COUNT=0\"><i>",count($list)-$i,' more available...</i></a>';
+	}
+
+	echo <<< EOB
+		</div>
+EOB;
+	break;
+
+
+// -----------------------------------------------
+// Per-Directory System Cache Entries
+// -----------------------------------------------
+case OB_SYS_CACHE_DIR:	
+	if (!$AUTHENTICATED) {
+		break;
+	}
+
+	echo <<<EOB
+		<div class=sorting><form>Scope:
+		<input type=hidden name=OB value={$MYREQUEST['OB']}>
+		<select name=SCOPE>
+EOB;
+	echo 
+		"<option value=A",$MYREQUEST['SCOPE']=='A' ? " selected":"",">Active</option>",
+		"<option value=D",$MYREQUEST['SCOPE']=='D' ? " selected":"",">Deleted</option>",
+		"</select>",
+		", Sorting:<select name=SORT1>",
+		"<option value=H",$MYREQUEST['SORT1']=='H' ? " selected":"",">Total Hits</option>",
+		"<option value=Z",$MYREQUEST['SORT1']=='Z' ? " selected":"",">Total Size</option>",
+		"<option value=T",$MYREQUEST['SORT1']=='T' ? " selected":"",">Number of Files</option>",
+		"<option value=S",$MYREQUEST['SORT1']=='S' ? " selected":"",">Directory Name</option>",
+		"<option value=A",$MYREQUEST['SORT1']=='A' ? " selected":"",">Avg. Size</option>",
+		"<option value=C",$MYREQUEST['SORT1']=='C' ? " selected":"",">Avg. Hits</option>",
+		'</select>',
+		'<select name=SORT2>',
+		'<option value=D',$MYREQUEST['SORT2']=='D' ? ' selected':'','>DESC</option>',
+		'<option value=A',$MYREQUEST['SORT2']=='A' ? ' selected':'','>ASC</option>',
+		'</select>',
+		'<select name=COUNT onChange="form.submit()">',
+		'<option value=10 ',$MYREQUEST['COUNT']=='10' ? ' selected':'','>Top 10</option>',
+		'<option value=20 ',$MYREQUEST['COUNT']=='20' ? ' selected':'','>Top 20</option>',
+		'<option value=50 ',$MYREQUEST['COUNT']=='50' ? ' selected':'','>Top 50</option>',
+		'<option value=100',$MYREQUEST['COUNT']=='100'? ' selected':'','>Top 100</option>',
+		'<option value=150',$MYREQUEST['COUNT']=='150'? ' selected':'','>Top 150</option>',
+		'<option value=200',$MYREQUEST['COUNT']=='200'? ' selected':'','>Top 200</option>',
+		'<option value=500',$MYREQUEST['COUNT']=='500'? ' selected':'','>Top 500</option>',
+		'<option value=0  ',$MYREQUEST['COUNT']=='0'  ? ' selected':'','>All</option>',
+		'</select>',
+		", Group By Dir Level:<select name=AGGR>",
+		"<option value='' selected>None</option>";
+		for ($i = 1; $i < 10; $i++)
+			echo "<option value=$i",$MYREQUEST['AGGR']==$i ? " selected":"",">$i</option>";
+		echo '</select>',
+		'&nbsp;<input type=submit value="GO!">',
+		'</form></div>',
+
+		'<div class="info"><table cellspacing=0><tbody>',
+		'<tr>',
+		'<th>',sortheader('S','Directory Name',	"&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('T','Number of Files',"&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('H','Total Hits',	"&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('Z','Total Size',	"&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('C','Avg. Hits',	"&OB=".$MYREQUEST['OB']),'</th>',
+		'<th>',sortheader('A','Avg. Size',	"&OB=".$MYREQUEST['OB']),'</th>',
+		'</tr>';
+
+	// builds list with alpha numeric sortable keys
+	//
+	$tmp = $list = array();
+	foreach($cache[$scope_list[$MYREQUEST['SCOPE']]] as $entry) {
+		$n = dirname($entry['filename']);
+		if ($MYREQUEST['AGGR'] > 0) {
+			$n = preg_replace("!^(/?(?:[^/\\\\]+[/\\\\]){".($MYREQUEST['AGGR']-1)."}[^/\\\\]*).*!", "$1", $n);
+		}
+		if (!isset($tmp[$n])) {
+			$tmp[$n] = array('hits'=>0,'size'=>0,'ents'=>0);
+		}
+		$tmp[$n]['hits'] += $entry['num_hits'];
+		$tmp[$n]['size'] += $entry['mem_size'];
+		++$tmp[$n]['ents'];
+	}
+
+	foreach ($tmp as $k => $v) {
+		switch($MYREQUEST['SORT1']) {
+			case 'A': $kn=sprintf('%015d-',$v['size'] / $v['ents']);break;
+			case 'T': $kn=sprintf('%015d-',$v['ents']);		break;
+			case 'H': $kn=sprintf('%015d-',$v['hits']);		break;
+			case 'Z': $kn=sprintf('%015d-',$v['size']);		break;
+			case 'C': $kn=sprintf('%015d-',$v['hits'] / $v['ents']);break;
+			case 'S': $kn = $k;					break;
+		}
+		$list[$kn.$k] = array($k, $v['ents'], $v['hits'], $v['size']);
+	}
+
+	if ($list) {
+		
+		// sort list
+		//
+		switch ($MYREQUEST['SORT2']) {
+			case "A":	krsort($list);	break;
+			case "D":	ksort($list);	break;
+		}
+		
+		// output list
+		$i = 0;
+		foreach($list as $entry) {
+			echo
+				'<tr class=tr-',$i%2,'>',
+				"<td class=td-0>",$entry[0],'</a></td>',
+				'<td class="td-n center">',$entry[1],'</td>',
+				'<td class="td-n center">',$entry[2],'</td>',
+				'<td class="td-n center">',$entry[3],'</td>',
+				'<td class="td-n center">',round($entry[2] / $entry[1]),'</td>',
+				'<td class="td-n center">',round($entry[3] / $entry[1]),'</td>',
+				'</tr>';
+
+			if (++$i == $MYREQUEST['COUNT']) break;
+		}
+		
+	} else {
+		echo '<tr class=tr-0><td class="center" colspan=6><i>No data</i></td></tr>';
+	}
+	echo <<< EOB
+		</tbody></table>
+EOB;
+
+	if ($list && $i < count($list)) {
+		echo "<a href=\"$MY_SELF&OB=",$MYREQUEST['OB'],"&COUNT=0\"><i>",count($list)-$i,' more available...</i></a>';
+	}
+
+	echo <<< EOB
+		</div>
+EOB;
+	break;
+
+// -----------------------------------------------
+// Version check
+// -----------------------------------------------
+case OB_VERSION_CHECK:
+	echo <<<EOB
+		<div class="info"><h2>APC Version Information</h2>
+		<table cellspacing=0><tbody>
+		<tr>
+		<th></th>
+		</tr>
+EOB;
+  if (defined('PROXY')) {
+    $ctxt = stream_context_create( array( 'http' => array( 'proxy' => PROXY, 'request_fulluri' => True ) ) );
+    $rss = @file_get_contents("http://pecl.php.net/feeds/pkg_apc.rss", False, $ctxt);
+  } else {
+    $rss = @file_get_contents("http://pecl.php.net/feeds/pkg_apc.rss");
+  }
+	if (!$rss) {
+		echo '<tr class="td-last center"><td>Unable to fetch version information.</td></tr>';
+	} else {
+		$apcversion = phpversion('apc');
+
+		preg_match('!<title>APC ([0-9.]+)</title>!', $rss, $match);
+		echo '<tr class="tr-0 center"><td>';
+		if (version_compare($apcversion, $match[1], '>=')) {
+			echo '<div class="ok">You are running the latest version of APC ('.$apcversion.')</div>';
+			$i = 3;
+		} else {
+			echo '<div class="failed">You are running an older version of APC ('.$apcversion.'), 
+				newer version '.$match[1].' is available at <a href="http://pecl.php.net/package/APC/'.$match[1].'">
+				http://pecl.php.net/package/APC/'.$match[1].'</a>
+				</div>';
+			$i = -1;
+		}
+		echo '</td></tr>';
+		echo '<tr class="tr-0"><td><h3>Change Log:</h3><br/>';
+
+		preg_match_all('!<(title|description)>([^<]+)</\\1>!', $rss, $match);
+		next($match[2]); next($match[2]);
+
+		while (list(,$v) = each($match[2])) {
+			list(,$ver) = explode(' ', $v, 2);
+			if ($i < 0 && version_compare($apcversion, $ver, '>=')) {
+				break;
+			} else if (!$i--) {
+				break;
+			}
+			echo "<b><a href=\"http://pecl.php.net/package/APC/$ver\">".htmlspecialchars($v, ENT_QUOTES, 'UTF-8')."</a></b><br><blockquote>";
+			echo nl2br(htmlspecialchars(current($match[2]), ENT_QUOTES, 'UTF-8'))."</blockquote>";
+			next($match[2]);
+		}
+		echo '</td></tr>';
+	}
+	echo <<< EOB
+		</tbody></table>
+		</div>
+EOB;
+	break;
+
+}
+
+echo <<< EOB
+	</div>
+EOB;
+
+?>
+
+<!-- <?php echo "\nBased on APCGUI By R.Becker\n$VERSION\n"?> -->
+</body>
+</html>

+ 47 - 0
hhvm/benchmark_config

@@ -0,0 +1,47 @@
+{
+  "framework": "php",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/json.php",
+      "plaintext_url": "/plaintext.php",
+      "db_url": "/dborm.php",
+      "query_url": "/dborm.php?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "MySQL",
+      "framework": "php",
+      "language": "PHP",
+      "orm": "Full",
+      "platform": "PHP-FPM",
+      "webserver": "nginx",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "php",
+      "notes": "",
+      "versus": "php"
+    },
+    "raw": {
+      "setup_file": "setup",
+      "db_url": "/dbraw.php",
+      "query_url": "/dbraw.php?queries=",
+      "fortune_url": "/fortune.php",
+      "update_url": "/updateraw.php?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "MySQL",
+      "framework": "php",
+      "language": "PHP",
+      "orm": "Raw",
+      "platform": "PHP-FPM",
+      "webserver": "nginx",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "php",
+      "notes": "",
+      "versus": "php"
+    }
+  }]
+}

+ 47 - 0
hhvm/dborm.php

@@ -0,0 +1,47 @@
+<?php
+//
+// Database Test
+//
+
+// Set content type
+header("Content-type: application/json");
+
+// Database connection
+// http://www.php.net/manual/en/ref.pdo-mysql.php
+// $pdo = new PDO('mysql:host=localhost;dbname=hello_world', 'benchmarkdbuser', 'benchmarkdbpass');
+
+# inclue the ActiveRecord library
+require_once 'php-activerecord/ActiveRecord.php';
+
+ActiveRecord\Config::initialize(function($cfg)
+{
+  $cfg->set_model_directory('models');
+  $cfg->set_connections(array('development' =>
+    'mysql://benchmarkdbuser:benchmarkdbpass@localhost/hello_world'));
+});
+
+// Read number of queries to run from URL parameter
+$query_count = 1;
+if (!empty($_GET)) {
+  $query_count = $_GET["queries"];
+}
+
+// Create an array with the response string.
+$arr = array();
+
+// For each query, store the result set values in the response array
+for ($i = 0; $i < $query_count; $i++) {
+  // Choose a random row
+  // http://www.php.net/mt_rand
+  $id = mt_rand(1, 10000);
+
+  $world = World::find_by_id($id);
+  
+  // Store result in array.
+  $arr[] = $world->to_json();
+}
+
+// Use the PHP standard JSON encoder.
+// http://www.php.net/manual/en/function.json-encode.php
+echo json_encode($arr);
+?>

+ 38 - 0
hhvm/dbraw.php

@@ -0,0 +1,38 @@
+<?php
+//
+// Database Test
+//
+
+// Database connection
+// http://www.php.net/manual/en/ref.pdo-mysql.php
+$pdo = new PDO('mysql:host=localhost;dbname=hello_world', 'benchmarkdbuser', 'benchmarkdbpass', array(
+    PDO::ATTR_PERSISTENT => true
+));
+
+// Read number of queries to run from URL parameter
+$query_count = 1;
+if (TRUE === isset($_GET['queries'])) {
+  $query_count = $_GET['queries'];
+}
+
+// Create an array with the response string.
+$arr = array();
+$id = mt_rand(1, 10000);
+
+// Define query
+$statement = $pdo->prepare('SELECT randomNumber FROM World WHERE id = :id');
+$statement->bindParam(':id', $id, PDO::PARAM_INT);
+
+// For each query, store the result set values in the response array
+while (0 < $query_count--) {
+  $statement->execute();
+  
+  // Store result in array.
+  $arr[] = array('id' => $id, 'randomNumber' => $statement->fetchColumn());
+  $id = mt_rand(1, 10000);
+}
+
+// Use the PHP standard JSON encoder.
+// http://www.php.net/manual/en/function.json-encode.php
+echo json_encode($arr);
+?>

+ 29 - 0
hhvm/deploy/config.hdf

@@ -0,0 +1,29 @@
+PidFile = /tmp/hhvm.pid
+
+Server {
+  Port = 8080
+  SourceRoot = /tmp/FrameworkBenchmarks
+  DefaultDocument = index.php
+}
+
+Log {
+  Level = None
+  NoSilencer = true
+  AlwaysLogUnhandledExceptions = true
+  RuntimeErrorReportingLevel = 8191
+  UseLogFile = true
+  UseSyslog = false
+  File = /tmp/hhvm-error.log
+  Access {
+    * {
+      File = /tmp/hhvm-access.log
+      Format = %h %l %u % t \”%r\” %>s %b
+    }
+  }
+}
+
+Repo {
+  Central {
+    Path = /tmp/.hhvm.hhbc
+  }
+}

+ 43 - 0
hhvm/fortune.php

@@ -0,0 +1,43 @@
+<?php
+//
+// Database Test
+//
+
+// Database connection
+// http://www.php.net/manual/en/ref.pdo-mysql.php
+$pdo = new PDO('mysql:host=localhost;dbname=hello_world;charset=utf8', 'benchmarkdbuser', 'benchmarkdbpass', array(
+    PDO::ATTR_PERSISTENT => true
+));
+
+// Define query
+$statement = $pdo->query( 'SELECT id, message FROM Fortune' );
+
+// Store result in array.
+$arr = $statement->fetchAll(PDO::FETCH_KEY_PAIR);
+$arr[0] = 'Additional fortune added at request time.';
+
+asort($arr);
+?>
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<title>Fortunes</title>
+</head>
+<body>
+<table>
+<tr>
+<th>id</th>
+<th>message</th>
+</tr>
+<?php
+foreach ( $arr as $id => &$fortune ) {
+?>
+<tr>
+<td><?php echo htmlspecialchars($id, ENT_QUOTES, 'utf-8'); ?></td>
+<td><?php echo htmlspecialchars($fortune, ENT_QUOTES, 'utf-8'); ?></td>
+</tr>
+<?php } ?>
+</table>
+</body>
+</html>

+ 17 - 0
hhvm/json.php

@@ -0,0 +1,17 @@
+<?php
+//
+// JSON Encoding Test
+//
+
+// Set content type
+header("Content-type: application/json");
+
+// Create an array with the response string.
+$arr = array(
+    "message" => "Hello, World!"
+);
+
+// Use the PHP standard JSON encoder.
+// http://www.php.net/manual/en/function.json-encode.php
+echo json_encode($arr);
+?>

+ 6 - 0
hhvm/models/World.php

@@ -0,0 +1,6 @@
+<?php
+class World extends ActiveRecord\Model
+{
+  static $table_name = 'World';
+}
+?>

+ 6 - 0
hhvm/php-activerecord/.gitignore

@@ -0,0 +1,6 @@
+.project
+.buildpath
+.settings
+*.log
+test/*.db
+*.swp

+ 49 - 0
hhvm/php-activerecord/ActiveRecord.php

@@ -0,0 +1,49 @@
+<?php
+if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50300)
+	die('PHP ActiveRecord requires PHP 5.3 or higher');
+
+define('PHP_ACTIVERECORD_VERSION_ID','1.0');
+
+if (!defined('PHP_ACTIVERECORD_AUTOLOAD_PREPEND'))
+	define('PHP_ACTIVERECORD_AUTOLOAD_PREPEND',true);
+
+require 'lib/Singleton.php';
+require 'lib/Config.php';
+require 'lib/Utils.php';
+require 'lib/DateTime.php';
+require 'lib/Model.php';
+require 'lib/Table.php';
+require 'lib/ConnectionManager.php';
+require 'lib/Connection.php';
+require 'lib/SQLBuilder.php';
+require 'lib/Reflections.php';
+require 'lib/Inflector.php';
+require 'lib/CallBack.php';
+require 'lib/Exceptions.php';
+require 'lib/Cache.php';
+
+if (!defined('PHP_ACTIVERECORD_AUTOLOAD_DISABLE'))
+	spl_autoload_register('activerecord_autoload',false,PHP_ACTIVERECORD_AUTOLOAD_PREPEND);
+
+function activerecord_autoload($class_name)
+{
+	$path = ActiveRecord\Config::instance()->get_model_directory();
+	$root = realpath(isset($path) ? $path : '.');
+
+	if (($namespaces = ActiveRecord\get_namespaces($class_name)))
+	{
+		$class_name = array_pop($namespaces);
+		$directories = array();
+
+		foreach ($namespaces as $directory)
+			$directories[] = $directory;
+
+		$root .= DIRECTORY_SEPARATOR . implode($directories, DIRECTORY_SEPARATOR);
+	}
+
+	$file = "$root/$class_name.php";
+
+	if (file_exists($file))
+		require $file;
+}
+?>

+ 25 - 0
hhvm/php-activerecord/CHANGELOG

@@ -0,0 +1,25 @@
+Version 1.0 - June 27, 2010
+
+- d2bed65 fixed an error with eager loading when no records exist
+- c225942 fixed set methods on DateTime objects to properly flag attributes as dirty
+- 46a1219 fixed a memory leak when using validations
+- c225942 fixed problem with some model functionality not working correctly after being deserialized 
+- 3e26749 fixed validates_numericality_of to not ignore other options when only_integer is present and matches
+- 53ad5ec fixed ambiguous id error when finding by pk with a join option
+- 26e40f4 fixed conditions to accept DateTime values
+- 41e52fe changed serialization to serialize datetime fields as strings instead of the actual DateTime objects
+- dbee94b Model::transaction() now returns true if commit was successful otherwise false
+
+Versio 1.0 RC1 - May 7, 2010
+
+- support for Oracle
+- support for PostgreSQL
+- added delegators
+- added setters
+- added getters
+- added HAVING as a finder option
+- added ability to find using a hash
+- added find_or_create_by
+- added validates_uniqueness_of
+- added dynamic count_by
+- added eager loading

+ 23 - 0
hhvm/php-activerecord/LICENSE

@@ -0,0 +1,23 @@
+Copyright (c) 2009
+
+AUTHORS:
+Kien La
+Jacques Fuentes
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 149 - 0
hhvm/php-activerecord/README.md

@@ -0,0 +1,149 @@
+# PHP ActiveRecord #
+
+Version 1.0
+
+by Kien La and Jacques Fuentes
+
+<http://www.phpactiverecord.org/>
+
+## Introduction ##
+A brief summarization of what ActiveRecord is:
+
+> Active record is an approach to access data in a database. A database table or view is wrapped into a class,
+> thus an object instance is tied to a single row in the table. After creation of an object, a new row is added to
+> the table upon save. Any object loaded gets its information from the database; when an object is updated, the
+> corresponding row in the table is also updated. The wrapper class implements accessor methods or properties for
+> each column in the table or view.
+
+More details can be found [here](http://en.wikipedia.org/wiki/Active_record_pattern).
+
+This implementation is inspired and thus borrows heavily from Ruby on Rails' ActiveRecord.
+We have tried to maintain their conventions while deviating mainly because of convenience or necessity.
+Of course, there are some differences which will be obvious to the user if they are familiar with rails.
+
+## Minimum Requirements ##
+
+- PHP 5.3+
+- PDO driver for your respective database
+
+## Supported Databases ##
+
+- MySQL
+- SQLite
+- PostgreSQL
+- Oracle
+
+# Features ##
+
+- Finder methods
+- Dynamic finder methods
+- Writer methods
+- Relationships
+- Validations
+- Callbacks
+- Serializations (json/xml)
+- Transactions
+- Support for multiple adapters
+- Miscellaneous options such as: aliased/protected/accessible attributes
+
+### Installation ##
+
+Setup is very easy and straight-forward. There are essentially only three configuration points you must concern yourself with:
+
+1. Setting the model auto_load directory.
+2. Configuring your database connections.
+3. Setting the database connection to use for your environment.
+
+Example:
+
+    ActiveRecord\Config::initialize(function($cfg)
+    {
+	    $cfg->set_model_directory('/path/to/your/model_directory');
+	    $cfg->set_connections(
+	      array(
+	        'development' => 'mysql://username:password@localhost/development_database_name',
+	        'test' => 'mysql://username:password@localhost/test_database_name',
+	        'production' => 'mysql://username:password@localhost/production_database_name'
+	      )
+	    );
+    });
+
+Alternatively (w/o the 5.3 closure):
+
+    $cfg = ActiveRecord\Config::instance();
+    $cfg->set_model_directory('/path/to/your/model_directory');
+    $cfg->set_connections(
+      array(
+        'development' => 'mysql://username:password@localhost/development_database_name',
+        'test' => 'mysql://username:password@localhost/test_database_name',
+        'production' => 'mysql://username:password@localhost/production_database_name'
+      )
+    );
+
+PHP ActiveRecord will default to use your development database. For testing or production, you simply set the default
+connection according to your current environment ('test' or 'production'):
+
+    ActiveRecord\Config::initialize(function($cfg) 
+    {
+      $cfg->set_default_connection(your_environment);
+    });
+
+Once you have configured these three settings you are done. ActiveRecord takes care of the rest for you.
+It does not require that you map your table schema to yaml/xml files. It will query the database for this information and
+cache it so that it does not make multiple calls to the database for a single schema.
+
+## Basic CRUD ##
+
+### Retrieve ###
+These are your basic methods to find and retrieve records from your database.
+See the *Finders* section for more details.
+
+	$post = Post::find(1);
+	echo $post->title; # 'My first blog post!!'
+	echo $post->author_id; # 5
+
+	# also the same since it is the first record in the db
+	$post = Post::first();
+
+	# finding using dynamic finders
+	$post = Post::find_by_name('The Decider');
+	$post = Post::find_by_name_and_id('The Bridge Builder',100);
+	$post = Post::find_by_name_or_id('The Bridge Builder',100);
+
+	# finding using a conditions array
+	$posts = Post::find('all',array('conditions' => array('name=? or id > ?','The Bridge Builder',100)));
+
+### Create ###
+Here we create a new post by instantiating a new object and then invoking the save() method.
+
+	$post = new Post();
+	$post->title = 'My first blog post!!';
+	$post->author_id = 5;
+	$post->save();
+	# INSERT INTO `posts` (title,author_id) VALUES('My first blog post!!', 5)
+
+### Update ###
+To update you would just need to find a record first and then change one of its attributes.
+It keeps an array of attributes that are "dirty" (that have been modified) and so our
+sql will only update the fields modified.
+
+	$post = Post::find(1);
+	echo $post->title; # 'My first blog post!!'
+	$post->title = 'Some real title';
+	$post->save();
+	# UPDATE `posts` SET title='Some real title' WHERE id=1
+
+	$post->title = 'New real title';
+	$post->author_id = 1;
+	$post->save();
+	# UPDATE `posts` SET title='New real title', author_id=1 WHERE id=1
+
+### Delete ###
+Deleting a record will not *destroy* the object. This means that it will call sql to delete
+the record in your database but you can still use the object if you need to.
+
+	$post = Post::find(1);
+	$post->delete();
+	# DELETE FROM `posts` WHERE id=1
+	echo $post->title; # 'New real title'
+

+ 16 - 0
hhvm/php-activerecord/composer.json

@@ -0,0 +1,16 @@
+{
+    "name": "php-activerecord/php-activerecord",
+    "type": "library",
+    "description": "php-activerecord is an open source ORM library based on the ActiveRecord pattern.",
+    "keywords": ["activerecord", "orm"],
+    "homepage": "http://www.phpactiverecord.org/",
+    "license": "MIT",
+    "require": {
+        "php": ">=5.3.0"
+    },
+    "autoload": {
+        "psr-0": {
+            "ActiveRecord": "lib/"
+        }
+    }
+}

+ 37 - 0
hhvm/php-activerecord/examples/orders/models/Order.php

@@ -0,0 +1,37 @@
+<?php
+class Order extends ActiveRecord\Model
+{
+	// order belongs to a person
+	static $belongs_to = array(
+		array('person'));
+
+	// order can have many payments by many people
+	// the conditions is just there as an example as it makes no logical sense
+	static $has_many = array(
+		array('payments'),
+		array('people',
+			'through'    => 'payments',
+			'select'     => 'people.*, payments.amount',
+			'conditions' => 'payments.amount < 200'));
+
+	// order must have a price and tax > 0
+	static $validates_numericality_of = array(
+		array('price', 'greater_than' => 0),
+		array('tax',   'greater_than' => 0));
+
+	// setup a callback to automatically apply a tax
+	static $before_validation_on_create = array('apply_tax');
+
+	public function apply_tax()
+	{
+		if ($this->person->state == 'VA')
+			$tax = 0.045;
+		elseif ($this->person->state == 'CA')
+			$tax = 0.10;
+		else
+			$tax = 0.02;
+
+		$this->tax = $this->price * $tax;
+	}
+}
+?>

+ 9 - 0
hhvm/php-activerecord/examples/orders/models/Payment.php

@@ -0,0 +1,9 @@
+<?php
+class Payment extends ActiveRecord\Model
+{
+	// payment belongs to a person
+	static $belongs_to = array(
+		array('person'),
+		array('order'));
+}
+?>

+ 13 - 0
hhvm/php-activerecord/examples/orders/models/Person.php

@@ -0,0 +1,13 @@
+<?php
+class Person extends ActiveRecord\Model
+{
+	// a person can have many orders and payments
+	static $has_many = array(
+		array('orders'),
+		array('payments'));
+
+	// must have a name and a state
+	static $validates_presence_of = array(
+		array('name'), array('state'));
+}
+?>

+ 79 - 0
hhvm/php-activerecord/examples/orders/orders.php

@@ -0,0 +1,79 @@
+<?php
+require_once __DIR__ . '/../../ActiveRecord.php';
+
+// initialize ActiveRecord
+ActiveRecord\Config::initialize(function($cfg)
+{
+    $cfg->set_model_directory(__DIR__ . '/models');
+    $cfg->set_connections(array('development' => 'mysql://test:[email protected]/orders_test'));
+
+	// you can change the default connection with the below
+    //$cfg->set_default_connection('production');
+});
+
+// create some people
+$jax = new Person(array('name' => 'Jax', 'state' => 'CA'));
+$jax->save();
+
+// compact way to create and save a model
+$tito = Person::create(array('name' => 'Tito', 'state' => 'VA'));
+
+// place orders. tax is automatically applied in a callback
+// create_orders will automatically place the created model into $tito->orders
+// even if it failed validation
+$pokemon = $tito->create_orders(array('item_name' => 'Live Pokemon', 'price' => 6999.99));
+$coal    = $tito->create_orders(array('item_name' => 'Lump of Coal', 'price' => 100.00));
+$freebie = $tito->create_orders(array('item_name' => 'Freebie', 'price' => -100.99));
+
+if (count($freebie->errors) > 0)
+	echo "[FAILED] saving order $freebie->item_name: " . join(', ',$freebie->errors->full_messages()) . "\n\n";
+
+// payments
+$pokemon->create_payments(array('amount' => 1.99, 'person_id' => $tito->id));
+$pokemon->create_payments(array('amount' => 4999.50, 'person_id' => $tito->id));
+$pokemon->create_payments(array('amount' => 2.50, 'person_id' => $jax->id));
+
+// reload since we don't want the freebie to show up (because it failed validation)
+$tito->reload();
+
+echo "$tito->name has " . count($tito->orders) . " orders for: " . join(', ',ActiveRecord\collect($tito->orders,'item_name')) . "\n\n";
+
+// get all orders placed by Tito
+foreach (Order::find_all_by_person_id($tito->id) as $order)
+{
+	echo "Order #$order->id for $order->item_name ($$order->price + $$order->tax tax) ordered by " . $order->person->name . "\n";
+
+	if (count($order->payments) > 0)
+	{
+		// display each payment for this order
+		foreach ($order->payments as $payment)
+			echo "  payment #$payment->id of $$payment->amount by " . $payment->person->name . "\n";
+	}
+	else
+		echo "  no payments\n";
+
+	echo "\n";
+}
+
+// display summary of all payments made by Tito and Jax
+$conditions = array(
+	'conditions'	=> array('id IN(?)',array($tito->id,$jax->id)),
+	'order'			=> 'name desc');
+
+foreach (Person::all($conditions) as $person)
+{
+	$n = count($person->payments);
+	$total = array_sum(ActiveRecord\collect($person->payments,'amount'));
+	echo "$person->name made $n payments for a total of $$total\n\n";
+}
+
+// using order has_many people through payments with options
+// array('people', 'through' => 'payments', 'select' => 'people.*, payments.amount', 'conditions' => 'payments.amount < 200'));
+// this means our people in the loop below also has the payment information since it is part of an inner join
+// we will only see 2 of the people instead of 3 because 1 of the payments is greater than 200
+$order = Order::find($pokemon->id);
+echo "Order #$order->id for $order->item_name ($$order->price + $$order->tax tax)\n";
+
+foreach ($order->people as $person)
+	echo "  payment of $$person->amount by " . $person->name . "\n";
+?>

+ 29 - 0
hhvm/php-activerecord/examples/orders/orders.sql

@@ -0,0 +1,29 @@
+-- written for mysql, not tested with any other db
+
+drop table if exists people;
+create table people(
+  id int not null primary key auto_increment,
+  name varchar(50),
+  state char(2),
+  created_at datetime,
+  updated_at datetime
+);
+
+drop table if exists orders;
+create table orders(
+  id int not null primary key auto_increment,
+  person_id int not null,
+  item_name varchar(50),
+  price decimal(10,2),
+  tax decimal(10,2),
+  created_at datetime
+);
+
+drop table if exists payments;
+create table payments(
+  id int not null primary key auto_increment,
+  order_id int not null,
+  person_id int not null,
+  amount decimal(10,2),
+  created_at datetime
+);

+ 17 - 0
hhvm/php-activerecord/examples/simple/simple.php

@@ -0,0 +1,17 @@
+<?php
+require_once __DIR__ . '/../../ActiveRecord.php';
+
+// assumes a table named "books" with a pk named "id"
+// see simple.sql
+class Book extends ActiveRecord\Model { }
+
+// initialize ActiveRecord
+// change the connection settings to whatever is appropriate for your mysql server 
+ActiveRecord\Config::initialize(function($cfg)
+{
+    $cfg->set_model_directory('.');
+    $cfg->set_connections(array('development' => 'mysql://test:[email protected]/test'));
+});
+
+print_r(Book::first()->attributes());
+?>

+ 7 - 0
hhvm/php-activerecord/examples/simple/simple.sql

@@ -0,0 +1,7 @@
+create table books(
+  id int not null primary key auto_increment,
+  name varchar(50),
+  author varchar(50)
+);
+
+insert into books(name,author) values('How to be Angry','Jax');

+ 32 - 0
hhvm/php-activerecord/examples/simple/simple_with_options.php

@@ -0,0 +1,32 @@
+<?php
+require_once __DIR__ . '/../../ActiveRecord.php';
+
+class Book extends ActiveRecord\Model
+{
+	// explicit table name since our table is not "books"
+	static $table_name = 'simple_book';
+
+	// explicit pk since our pk is not "id"
+	static $primary_key = 'book_id';
+
+	// explicit connection name since we always want production with this model
+	static $connection = 'production';
+
+	// explicit database name will generate sql like so => db.table_name
+	static $db = 'test';
+}
+
+$connections = array(
+	'development' => 'mysql://invalid',
+	'production' => 'mysql://test:[email protected]/test'
+);
+
+// initialize ActiveRecord
+ActiveRecord\Config::initialize(function($cfg) use ($connections)
+{
+    $cfg->set_model_directory('.');
+    $cfg->set_connections($connections);
+});
+
+print_r(Book::first()->attributes());
+?>

+ 6 - 0
hhvm/php-activerecord/examples/simple/simple_with_options.sql

@@ -0,0 +1,6 @@
+create table simple_book(
+  book_id int not null primary key auto_increment,
+  name varchar(50)
+);
+
+insert into simple_book (name) values ('simple w/ options!');

+ 80 - 0
hhvm/php-activerecord/lib/Cache.php

@@ -0,0 +1,80 @@
+<?php
+namespace ActiveRecord;
+use Closure;
+
+/**
+ * Cache::get('the-cache-key', function() {
+ *	 # this gets executed when cache is stale
+ *	 return "your cacheable datas";
+ * });
+ */
+class Cache
+{
+	static $adapter = null;
+	static $options = array();
+
+	/**
+	 * Initializes the cache.
+	 *
+	 * With the $options array it's possible to define:
+	 * - expiration of the key, (time in seconds)
+	 * - a namespace for the key
+	 *
+	 * this last one is useful in the case two applications use
+	 * a shared key/store (for instance a shared Memcached db)
+	 *
+	 * Ex:
+	 * $cfg_ar = ActiveRecord\Config::instance();
+	 * $cfg_ar->set_cache('memcache://localhost:11211',array('namespace' => 'my_cool_app',
+	 *																											 'expire'		 => 120
+	 *																											 ));
+	 *
+	 * In the example above all the keys expire after 120 seconds, and the
+	 * all get a postfix 'my_cool_app'.
+	 *
+	 * (Note: expiring needs to be implemented in your cache store.)
+	 *
+	 * @param string $url URL to your cache server
+	 * @param array $options Specify additional options
+	 */
+	public static function initialize($url, $options=array())
+	{
+		if ($url)
+		{
+			$url = parse_url($url);
+			$file = ucwords(Inflector::instance()->camelize($url['scheme']));
+			$class = "ActiveRecord\\$file";
+			require_once __DIR__ . "/cache/$file.php";
+			static::$adapter = new $class($url);
+		}
+		else
+			static::$adapter = null;
+
+		static::$options = array_merge(array('expire' => 30, 'namespace' => ''),$options);
+	}
+
+	public static function flush()
+	{
+		if (static::$adapter)
+			static::$adapter->flush();
+	}
+
+	public static function get($key, $closure)
+	{
+		$key = static::get_namespace() . $key;
+		
+		if (!static::$adapter)
+			return $closure();
+
+		if (!($value = static::$adapter->read($key)))
+			static::$adapter->write($key,($value = $closure()),static::$options['expire']);
+
+		return $value;
+	}
+
+	private static function get_namespace()
+	{
+		return (isset(static::$options['namespace']) && strlen(static::$options['namespace']) > 0) ? (static::$options['namespace'] . "::") : "";
+	}
+}
+?>

+ 252 - 0
hhvm/php-activerecord/lib/CallBack.php

@@ -0,0 +1,252 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+use Closure;
+
+/**
+ * Callbacks allow the programmer to hook into the life cycle of a {@link Model}.
+ *
+ * You can control the state of your object by declaring certain methods to be
+ * called before or after methods are invoked on your object inside of ActiveRecord.
+ *
+ * Valid callbacks are:
+ * <ul>
+ * <li><b>after_construct:</b> called after a model has been constructed</li>
+ * <li><b>before_save:</b> called before a model is saved</li>
+ * <li><b>after_save:</b> called after a model is saved</li>
+ * <li><b>before_create:</b> called before a NEW model is to be inserted into the database</li>
+ * <li><b>after_create:</b> called after a NEW model has been inserted into the database</li>
+ * <li><b>before_update:</b> called before an existing model has been saved</li>
+ * <li><b>after_update:</b> called after an existing model has been saved</li>
+ * <li><b>before_validation:</b> called before running validators</li>
+ * <li><b>after_validation:</b> called after running validators</li>
+ * <li><b>before_validation_on_create:</b> called before validation on a NEW model being inserted</li>
+ * <li><b>after_validation_on_create:</b> called after validation on a NEW model being inserted</li>
+ * <li><b>before_validation_on_update:</b> see above except for an existing model being saved</li>
+ * <li><b>after_validation_on_update:</b> ...</li>
+ * <li><b>before_destroy:</b> called after a model has been deleted</li>
+ * <li><b>after_destroy:</b> called after a model has been deleted</li>
+ * </ul>
+ *
+ * This class isn't meant to be used directly. Callbacks are defined on your model like the example below:
+ *
+ * <code>
+ * class Person extends ActiveRecord\Model {
+ *   static $before_save = array('make_name_uppercase');
+ *   static $after_save = array('do_happy_dance');
+ *
+ *   public function make_name_uppercase() {
+ *     $this->name = strtoupper($this->name);
+ *   }
+ *
+ *   public function do_happy_dance() {
+ *     happy_dance();
+ *   }
+ * }
+ * </code>
+ *
+ * Available options for callbacks:
+ *
+ * <ul>
+ * <li><b>prepend:</b> puts the callback at the top of the callback chain instead of the bottom</li>
+ * </ul>
+ *
+ * @package ActiveRecord
+ * @link http://www.phpactiverecord.org/guides/callbacks
+ */
+class CallBack
+{
+	/**
+	 * List of available callbacks.
+	 *
+	 * @var array
+	 */
+	static protected $VALID_CALLBACKS = array(
+		'after_construct',
+		'before_save',
+		'after_save',
+		'before_create',
+		'after_create',
+		'before_update',
+		'after_update',
+		'before_validation',
+		'after_validation',
+		'before_validation_on_create',
+		'after_validation_on_create',
+		'before_validation_on_update',
+		'after_validation_on_update',
+		'before_destroy',
+		'after_destroy'
+	);
+
+	/**
+	 * Container for reflection class of given model
+	 *
+	 * @var object
+	 */
+	private $klass;
+
+	/**
+	 * List of public methods of the given model
+	 * @var array
+	 */
+	private $publicMethods;
+
+	/**
+	 * Holds data for registered callbacks.
+	 *
+	 * @var array
+	 */
+	private $registry = array();
+
+	/**
+	 * Creates a CallBack.
+	 *
+	 * @param string $model_class_name The name of a {@link Model} class
+	 * @return CallBack
+	 */
+	public function __construct($model_class_name)
+	{
+		$this->klass = Reflections::instance()->get($model_class_name);
+
+		foreach (static::$VALID_CALLBACKS as $name)
+		{
+			// look for explicitly defined static callback
+			if (($definition = $this->klass->getStaticPropertyValue($name,null)))
+			{
+				if (!is_array($definition))
+					$definition = array($definition);
+
+				foreach ($definition as $method_name)
+					$this->register($name,$method_name);
+			}
+
+			// implicit callbacks that don't need to have a static definition
+			// simply define a method named the same as something in $VALID_CALLBACKS
+			// and the callback is auto-registered
+			elseif ($this->klass->hasMethod($name))
+				$this->register($name,$name);
+		}
+	}
+
+	/**
+	 * Returns all the callbacks registered for a callback type.
+	 *
+	 * @param $name string Name of a callback (see {@link VALID_CALLBACKS $VALID_CALLBACKS})
+	 * @return array array of callbacks or null if invalid callback name.
+	 */
+	public function get_callbacks($name)
+	{
+		return isset($this->registry[$name]) ? $this->registry[$name] : null;
+	}
+
+	/**
+	 * Invokes a callback.
+	 *
+	 * @internal This is the only piece of the CallBack class that carries its own logic for the
+	 * model object. For (after|before)_(create|update) callbacks, it will merge with
+	 * a generic 'save' callback which is called first for the lease amount of precision.
+	 *
+	 * @param string $model Model to invoke the callback on.
+	 * @param string $name Name of the callback to invoke
+	 * @param boolean $must_exist Set to true to raise an exception if the callback does not exist.
+	 * @return mixed null if $name was not a valid callback type or false if a method was invoked
+	 * that was for a before_* callback and that method returned false. If this happens, execution
+	 * of any other callbacks after the offending callback will not occur.
+	 */
+	public function invoke($model, $name, $must_exist=true)
+	{
+		if ($must_exist && !array_key_exists($name, $this->registry))
+			throw new ActiveRecordException("No callbacks were defined for: $name on " . get_class($model));
+
+		// if it doesn't exist it might be a /(after|before)_(create|update)/ so we still need to run the save
+		// callback
+		if (!array_key_exists($name, $this->registry))
+			$registry = array();
+		else
+			$registry = $this->registry[$name];
+
+		$first = substr($name,0,6);
+
+		// starts with /(after|before)_(create|update)/
+		if (($first == 'after_' || $first == 'before') && (($second = substr($name,7,5)) == 'creat' || $second == 'updat' || $second == 'reate' || $second == 'pdate'))
+		{
+			$temporal_save = str_replace(array('create', 'update'), 'save', $name);
+
+			if (!isset($this->registry[$temporal_save]))
+				$this->registry[$temporal_save] = array();
+
+			$registry = array_merge($this->registry[$temporal_save], $registry ? $registry : array());
+		}
+
+		if ($registry)
+		{
+			foreach ($registry as $method)
+			{
+				$ret = ($method instanceof Closure ? $method($model) : $model->$method());
+
+				if (false === $ret && $first === 'before')
+					return false;
+			}
+		}
+		return true;
+	}
+
+	/**
+	 * Register a new callback.
+	 *
+	 * The option array can contain the following parameters:
+	 * <ul>
+	 * <li><b>prepend:</b> Add this callback at the beginning of the existing callbacks (true) or at the end (false, default)</li>
+	 * </ul>
+	 *
+	 * @param string $name Name of callback type (see {@link VALID_CALLBACKS $VALID_CALLBACKS})
+	 * @param mixed $closure_or_method_name Either a closure or the name of a method on the {@link Model}
+	 * @param array $options Options array
+	 * @return void
+	 * @throws ActiveRecordException if invalid callback type or callback method was not found
+	 */
+	public function register($name, $closure_or_method_name=null, $options=array())
+	{
+		$options = array_merge(array('prepend' => false), $options);
+
+		if (!$closure_or_method_name)
+			$closure_or_method_name = $name;
+
+		if (!in_array($name,self::$VALID_CALLBACKS))
+			throw new ActiveRecordException("Invalid callback: $name");
+
+		if (!($closure_or_method_name instanceof Closure))
+		{
+			if (!isset($this->publicMethods))
+				$this->publicMethods = get_class_methods($this->klass->getName());
+
+			if (!in_array($closure_or_method_name, $this->publicMethods))
+			{
+				if ($this->klass->hasMethod($closure_or_method_name))
+				{
+					// Method is private or protected
+					throw new ActiveRecordException("Callback methods need to be public (or anonymous closures). " .
+						"Please change the visibility of " . $this->klass->getName() . "->" . $closure_or_method_name . "()");
+				}
+				else
+				{
+					// i'm a dirty ruby programmer
+					throw new ActiveRecordException("Unknown method for callback: $name" .
+						(is_string($closure_or_method_name) ? ": #$closure_or_method_name" : ""));
+				}
+			}
+		}
+
+		if (!isset($this->registry[$name]))
+			$this->registry[$name] = array();
+
+		if ($options['prepend'])
+			array_unshift($this->registry[$name], $closure_or_method_name);
+		else
+			$this->registry[$name][] = $closure_or_method_name;
+	}
+}
+?>

+ 155 - 0
hhvm/php-activerecord/lib/Column.php

@@ -0,0 +1,155 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Class for a table column.
+ *
+ * @package ActiveRecord
+ */
+class Column
+{
+	// types for $type
+	const STRING	= 1;
+	const INTEGER	= 2;
+	const DECIMAL	= 3;
+	const DATETIME	= 4;
+	const DATE		= 5;
+	const TIME		= 6;
+
+	/**
+	 * Map a type to an column type.
+	 * @static
+	 * @var array
+	 */
+	static $TYPE_MAPPING = array(
+		'datetime'	=> self::DATETIME,
+		'timestamp'	=> self::DATETIME,
+		'date'		=> self::DATE,
+		'time'		=> self::TIME,
+
+		'int'		=> self::INTEGER,
+		'tinyint'	=> self::INTEGER,
+		'smallint'	=> self::INTEGER,
+		'mediumint'	=> self::INTEGER,
+		'bigint'	=> self::INTEGER,
+
+		'float'		=> self::DECIMAL,
+		'double'	=> self::DECIMAL,
+		'numeric'	=> self::DECIMAL,
+		'decimal'	=> self::DECIMAL,
+		'dec'		=> self::DECIMAL);
+
+	/**
+	 * The true name of this column.
+	 * @var string
+	 */
+	public $name;
+
+	/**
+	 * The inflected name of this columns .. hyphens/spaces will be => _.
+	 * @var string
+	 */
+	public $inflected_name;
+
+	/**
+	 * The type of this column: STRING, INTEGER, ...
+	 * @var integer
+	 */
+	public $type;
+
+	/**
+	 * The raw database specific type.
+	 * @var string
+	 */
+	public $raw_type;
+
+	/**
+	 * The maximum length of this column.
+	 * @var int
+	 */
+	public $length;
+
+	/**
+	 * True if this column allows null.
+	 * @var boolean
+	 */
+	public $nullable;
+
+	/**
+	 * True if this column is a primary key.
+	 * @var boolean
+	 */
+	public $pk;
+
+	/**
+	 * The default value of the column.
+	 * @var mixed
+	 */
+	public $default;
+
+	/**
+	 * True if this column is set to auto_increment.
+	 * @var boolean
+	 */
+	public $auto_increment;
+
+	/**
+	 * Name of the sequence to use for this column if any.
+	 * @var boolean
+	 */
+	public $sequence;
+
+	/**
+	 * Casts a value to the column's type.
+	 *
+	 * @param mixed $value The value to cast
+	 * @param Connection $connection The Connection this column belongs to
+	 * @return mixed type-casted value
+	 */
+	public function cast($value, $connection)
+	{
+		if ($value === null)
+			return null;
+
+		switch ($this->type)
+		{
+			case self::STRING:	return (string)$value;
+			case self::INTEGER:	return (int)$value;
+			case self::DECIMAL:	return (double)$value;
+			case self::DATETIME:
+			case self::DATE:
+				if (!$value)
+					return null;
+
+				if ($value instanceof DateTime)
+					return $value;
+
+				if ($value instanceof \DateTime)
+					return new DateTime($value->format('Y-m-d H:i:s T'));
+
+				return $connection->string_to_datetime($value);
+		}
+		return $value;
+	}
+
+	/**
+	 * Sets the $type member variable.
+	 * @return mixed
+	 */
+	public function map_raw_type()
+	{
+		if ($this->raw_type == 'integer')
+			$this->raw_type = 'int';
+
+		if (array_key_exists($this->raw_type,self::$TYPE_MAPPING))
+			$this->type = self::$TYPE_MAPPING[$this->raw_type];
+		else
+			$this->type = self::STRING;
+
+		return $this->type;
+	}
+}
+?>

+ 304 - 0
hhvm/php-activerecord/lib/Config.php

@@ -0,0 +1,304 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+use Closure;
+
+/**
+ * Manages configuration options for ActiveRecord.
+ *
+ * <code>
+ * ActiveRecord::initialize(function($cfg) {
+ *   $cfg->set_model_home('models');
+ *   $cfg->set_connections(array(
+ *     'development' => 'mysql://user:[email protected]/awesome_development',
+ *     'production' => 'mysql://user:[email protected]/awesome_production'));
+ * });
+ * </code>
+ *
+ * @package ActiveRecord
+ */
+class Config extends Singleton
+{
+	/**
+	 * Name of the connection to use by default.
+	 *
+	 * <code>
+	 * ActiveRecord\Config::initialize(function($cfg) {
+	 *   $cfg->set_model_directory('/your/app/models');
+	 *   $cfg->set_connections(array(
+	 *     'development' => 'mysql://user:[email protected]/awesome_development',
+	 *     'production' => 'mysql://user:[email protected]/awesome_production'));
+	 * });
+	 * </code>
+	 *
+	 * This is a singleton class so you can retrieve the {@link Singleton} instance by doing:
+	 *
+	 * <code>
+	 * $config = ActiveRecord\Config::instance();
+	 * </code>
+	 *
+	 * @var string
+	 */
+	private $default_connection = 'development';
+
+	/**
+	 * Contains the list of database connection strings.
+	 *
+	 * @var array
+	 */
+	private $connections = array();
+
+	/**
+	 * Directory for the auto_loading of model classes.
+	 *
+	 * @see activerecord_autoload
+	 * @var string
+	 */
+	private $model_directory;
+
+	/**
+	 * Switch for logging.
+	 *
+	 * @var bool
+	 */
+	private $logging = false;
+
+	/**
+	 * Contains a Logger object that must impelement a log() method.
+	 *
+	 * @var object
+	 */
+	private $logger;
+
+	/**
+	 * The format to serialize DateTime values into.
+	 *
+	 * @var string
+	 */
+	private $date_format = \DateTime::ISO8601;
+
+	/**
+	 * Allows config initialization using a closure.
+	 *
+	 * This method is just syntatic sugar.
+	 *
+	 * <code>
+	 * ActiveRecord\Config::initialize(function($cfg) {
+	 *   $cfg->set_model_directory('/path/to/your/model_directory');
+	 *   $cfg->set_connections(array(
+	 *     'development' => 'mysql://username:[email protected]/database_name'));
+	 * });
+	 * </code>
+	 *
+	 * You can also initialize by grabbing the singleton object:
+	 *
+	 * <code>
+	 * $cfg = ActiveRecord\Config::instance();
+	 * $cfg->set_model_directory('/path/to/your/model_directory');
+	 * $cfg->set_connections(array('development' =>
+  	 *   'mysql://username:password@localhost/database_name'));
+	 * </code>
+	 *
+	 * @param Closure $initializer A closure
+	 * @return void
+	 */
+	public static function initialize(Closure $initializer)
+	{
+		$initializer(parent::instance());
+	}
+
+	/**
+	 * Sets the list of database connection strings.
+	 *
+	 * <code>
+	 * $config->set_connections(array(
+	 *     'development' => 'mysql://username:[email protected]/database_name'));
+	 * </code>
+	 *
+	 * @param array $connections Array of connections
+	 * @param string $default_connection Optionally specify the default_connection
+	 * @return void
+	 * @throws ActiveRecord\ConfigException
+	 */
+	public function set_connections($connections, $default_connection=null)
+	{
+		if (!is_array($connections))
+			throw new ConfigException("Connections must be an array");
+
+		if ($default_connection)
+			$this->set_default_connection($default_connection);
+
+		$this->connections = $connections;
+	}
+
+	/**
+	 * Returns the connection strings array.
+	 *
+	 * @return array
+	 */
+	public function get_connections()
+	{
+		return $this->connections;
+	}
+
+	/**
+	 * Returns a connection string if found otherwise null.
+	 *
+	 * @param string $name Name of connection to retrieve
+	 * @return string connection info for specified connection name
+	 */
+	public function get_connection($name)
+	{
+		if (array_key_exists($name, $this->connections))
+			return $this->connections[$name];
+
+		return null;
+	}
+
+	/**
+	 * Returns the default connection string or null if there is none.
+	 *
+	 * @return string
+	 */
+	public function get_default_connection_string()
+	{
+		return array_key_exists($this->default_connection,$this->connections) ?
+			$this->connections[$this->default_connection] : null;
+	}
+
+	/**
+	 * Returns the name of the default connection.
+	 *
+	 * @return string
+	 */
+	public function get_default_connection()
+	{
+		return $this->default_connection;
+	}
+
+	/**
+	 * Set the name of the default connection.
+	 *
+	 * @param string $name Name of a connection in the connections array
+	 * @return void
+	 */
+	public function set_default_connection($name)
+	{
+		$this->default_connection = $name;
+	}
+
+	/**
+	 * Sets the directory where models are located.
+	 *
+	 * @param string $dir Directory path containing your models
+	 * @return void
+	 */
+	public function set_model_directory($dir)
+	{
+		$this->model_directory = $dir;
+	}
+
+	/**
+	 * Returns the model directory.
+	 *
+	 * @return string
+	 * @throws ConfigException if specified directory was not found
+	 */
+	public function get_model_directory()
+	{
+		if ($this->model_directory && !file_exists($this->model_directory))
+			throw new ConfigException('Invalid or non-existent directory: '.$this->model_directory);
+
+		return $this->model_directory;
+	}
+
+	/**
+	 * Turn on/off logging
+	 *
+	 * @param boolean $bool
+	 * @return void
+	 */
+	public function set_logging($bool)
+	{
+		$this->logging = (bool)$bool;
+	}
+
+	/**
+	 * Sets the logger object for future SQL logging
+	 *
+	 * @param object $logger
+	 * @return void
+	 * @throws ConfigException if Logger objecct does not implement public log()
+	 */
+	public function set_logger($logger)
+	{
+		$klass = Reflections::instance()->add($logger)->get($logger);
+
+		if (!$klass->getMethod('log') || !$klass->getMethod('log')->isPublic())
+			throw new ConfigException("Logger object must implement a public log method");
+
+		$this->logger = $logger;
+	}
+
+	/**
+	 * Return whether or not logging is on
+	 *
+	 * @return boolean
+	 */
+	public function get_logging()
+	{
+		return $this->logging;
+	}
+
+	/**
+	 * Returns the logger
+	 *
+	 * @return object
+	 */
+	public function get_logger()
+	{
+		return $this->logger;
+	}
+
+	/**
+	 * @deprecated
+	 */
+	public function get_date_format()
+	{
+		trigger_error('Use ActiveRecord\Serialization::$DATETIME_FORMAT. Config::get_date_format() has been deprecated.', E_USER_DEPRECATED);
+		return Serialization::$DATETIME_FORMAT;
+	}
+
+	/**
+	 * @deprecated
+	 */
+	public function set_date_format($format)
+	{
+		trigger_error('Use ActiveRecord\Serialization::$DATETIME_FORMAT. Config::set_date_format() has been deprecated.', E_USER_DEPRECATED);
+		Serialization::$DATETIME_FORMAT = $format;
+	}
+
+	/**
+	 * Sets the url for the cache server to enable query caching.
+	 *
+	 * Only table schema queries are cached at the moment. A general query cache
+	 * will follow.
+	 *
+	 * Example:
+	 *
+	 * <code>
+	 * $config->set_cache("memcached://localhost");
+	 * $config->set_cache("memcached://localhost",array("expire" => 60));
+	 * </code>
+	 *
+	 * @param string $url Url to your cache server.
+	 * @param array $options Array of options
+	 */
+	public function set_cache($url, $options=array())
+	{
+		Cache::initialize($url,$options);
+	}
+};
+?>

+ 523 - 0
hhvm/php-activerecord/lib/Connection.php

@@ -0,0 +1,523 @@
+<?php
+
+/**
+ * @package ActiveRecord
+ */
+
+namespace ActiveRecord;
+
+require 'Column.php';
+
+use PDO;
+use PDOException;
+use Closure;
+
+/**
+ * The base class for database connection adapters.
+ *
+ * @package ActiveRecord
+ */
+abstract class Connection
+{
+
+	/**
+	 * The PDO connection object.
+	 * @var mixed
+	 */
+	public $connection;
+	/**
+	 * The last query run.
+	 * @var string
+	 */
+	public $last_query;
+	/**
+	 * Switch for logging.
+	 *
+	 * @var bool
+	 */
+	private $logging = false;
+	/**
+	 * Contains a Logger object that must impelement a log() method.
+	 *
+	 * @var object
+	 */
+	private $logger;
+	/**
+	 * The name of the protocol that is used.
+	 * @var string
+	 */
+	public $protocol;
+	/**
+	 * Default PDO options to set for each connection.
+	 * @var array
+	 */
+	static $PDO_OPTIONS = array(
+		PDO::ATTR_CASE => PDO::CASE_LOWER,
+		PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+		PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
+		PDO::ATTR_STRINGIFY_FETCHES => false);
+	/**
+	 * The quote character for stuff like column and field names.
+	 * @var string
+	 */
+	static $QUOTE_CHARACTER = '`';
+	/**
+	 * Default port.
+	 * @var int
+	 */
+	static $DEFAULT_PORT = 0;
+
+	/**
+	 * Retrieve a database connection.
+	 *
+	 * @param string $connection_string_or_connection_name A database connection string (ex. mysql://user:pass@host[:port]/dbname)
+	 *   Everything after the protocol:// part is specific to the connection adapter.
+	 *   OR
+	 *   A connection name that is set in ActiveRecord\Config
+	 *   If null it will use the default connection specified by ActiveRecord\Config->set_default_connection
+	 * @return Connection
+	 * @see parse_connection_url
+	 */
+	public static function instance($connection_string_or_connection_name=null)
+	{
+		$config = Config::instance();
+
+		if (strpos($connection_string_or_connection_name, '://') === false)
+		{
+			$connection_string = $connection_string_or_connection_name ?
+				$config->get_connection($connection_string_or_connection_name) :
+				$config->get_default_connection_string();
+		}
+		else
+			$connection_string = $connection_string_or_connection_name;
+
+		if (!$connection_string)
+			throw new DatabaseException("Empty connection string");
+
+		$info = static::parse_connection_url($connection_string);
+		$fqclass = static::load_adapter_class($info->protocol);
+
+		try {
+			$connection = new $fqclass($info);
+			$connection->protocol = $info->protocol;
+			$connection->logging = $config->get_logging();
+			$connection->logger = $connection->logging ? $config->get_logger() : null;
+
+			if (isset($info->charset))
+				$connection->set_encoding($info->charset);
+		} catch (PDOException $e) {
+			throw new DatabaseException($e);
+		}
+		return $connection;
+	}
+
+	/**
+	 * Loads the specified class for an adapter.
+	 *
+	 * @param string $adapter Name of the adapter.
+	 * @return string The full name of the class including namespace.
+	 */
+	private static function load_adapter_class($adapter)
+	{
+		$class = ucwords($adapter) . 'Adapter';
+		$fqclass = 'ActiveRecord\\' . $class;
+		$source = __DIR__ . "/adapters/$class.php";
+
+		if (!file_exists($source))
+			throw new DatabaseException("$fqclass not found!");
+
+		require_once($source);
+		return $fqclass;
+	}
+
+	/**
+	 * Use this for any adapters that can take connection info in the form below
+	 * to set the adapters connection info.
+	 *
+	 * <code>
+	 * protocol://username:password@host[:port]/dbname
+	 * protocol://urlencoded%20username:urlencoded%20password@host[:port]/dbname?decode=true
+	 * protocol://username:password@unix(/some/file/path)/dbname
+	 * </code>
+	 *
+	 * Sqlite has a special syntax, as it does not need a database name or user authentication:
+	 *
+	 * <code>
+	 * sqlite://file.db
+	 * sqlite://../relative/path/to/file.db
+	 * sqlite://unix(/absolute/path/to/file.db)
+	 * sqlite://windows(c%2A/absolute/path/to/file.db)
+	 * </code>
+	 *
+	 * @param string $connection_url A connection URL
+	 * @return object the parsed URL as an object.
+	 */
+	public static function parse_connection_url($connection_url)
+	{
+		$url = @parse_url($connection_url);
+
+		if (!isset($url['host']))
+			throw new DatabaseException('Database host must be specified in the connection string. If you want to specify an absolute filename, use e.g. sqlite://unix(/path/to/file)');
+
+		$info = new \stdClass();
+		$info->protocol = $url['scheme'];
+		$info->host = $url['host'];
+		$info->db = isset($url['path']) ? substr($url['path'], 1) : null;
+		$info->user = isset($url['user']) ? $url['user'] : null;
+		$info->pass = isset($url['pass']) ? $url['pass'] : null;
+
+		$allow_blank_db = ($info->protocol == 'sqlite');
+
+		if ($info->host == 'unix(')
+		{
+			$socket_database = $info->host . '/' . $info->db;
+
+			if ($allow_blank_db)
+				$unix_regex = '/^unix\((.+)\)\/?().*$/';
+			else
+				$unix_regex = '/^unix\((.+)\)\/(.+)$/';
+
+			if (preg_match_all($unix_regex, $socket_database, $matches) > 0)
+			{
+				$info->host = $matches[1][0];
+				$info->db = $matches[2][0];
+			}
+		} elseif (substr($info->host, 0, 8) == 'windows(')
+		{
+			$info->host = urldecode(substr($info->host, 8) . '/' . substr($info->db, 0, -1));
+			$info->db = null;
+		}
+
+		if ($allow_blank_db && $info->db)
+			$info->host .= '/' . $info->db;
+
+		if (isset($url['port']))
+			$info->port = $url['port'];
+
+		if (strpos($connection_url, 'decode=true') !== false)
+		{
+			if ($info->user)
+				$info->user = urldecode($info->user);
+
+			if ($info->pass)
+				$info->pass = urldecode($info->pass);
+		}
+
+		if (isset($url['query']))
+		{
+			foreach (explode('/&/', $url['query']) as $pair) {
+				list($name, $value) = explode('=', $pair);
+
+				if ($name == 'charset')
+					$info->charset = $value;
+			}
+		}
+
+		return $info;
+	}
+
+	/**
+	 * Class Connection is a singleton. Access it via instance().
+	 *
+	 * @param array $info Array containing URL parts
+	 * @return Connection
+	 */
+	protected function __construct($info)
+	{
+		try {
+			// unix sockets start with a /
+			if ($info->host[0] != '/')
+			{
+				$host = "host=$info->host";
+
+				if (isset($info->port))
+					$host .= ";port=$info->port";
+			}
+			else
+				$host = "unix_socket=$info->host";
+
+			$this->connection = new PDO("$info->protocol:$host;dbname=$info->db", $info->user, $info->pass, static::$PDO_OPTIONS);
+		} catch (PDOException $e) {
+			throw new DatabaseException($e);
+		}
+	}
+
+	/**
+	 * Retrieves column meta data for the specified table.
+	 *
+	 * @param string $table Name of a table
+	 * @return array An array of {@link Column} objects.
+	 */
+	public function columns($table)
+	{
+		$columns = array();
+		$sth = $this->query_column_info($table);
+
+		while (($row = $sth->fetch())) {
+			$c = $this->create_column($row);
+			$columns[$c->name] = $c;
+		}
+		return $columns;
+	}
+
+	/**
+	 * Escapes quotes in a string.
+	 *
+	 * @param string $string The string to be quoted.
+	 * @return string The string with any quotes in it properly escaped.
+	 */
+	public function escape($string)
+	{
+		return $this->connection->quote($string);
+	}
+
+	/**
+	 * Retrieve the insert id of the last model saved.
+	 *
+	 * @param string $sequence Optional name of a sequence to use
+	 * @return int
+	 */
+	public function insert_id($sequence=null)
+	{
+		return $this->connection->lastInsertId($sequence);
+	}
+
+	/**
+	 * Execute a raw SQL query on the database.
+	 *
+	 * @param string $sql Raw SQL string to execute.
+	 * @param array &$values Optional array of bind values
+	 * @return mixed A result set object
+	 */
+	public function query($sql, &$values=array())
+	{
+		if ($this->logging)
+			$this->logger->log($sql);
+
+		$this->last_query = $sql;
+
+		try {
+			if (!($sth = $this->connection->prepare($sql)))
+				throw new DatabaseException($this);
+		} catch (PDOException $e) {
+			throw new DatabaseException($this);
+		}
+
+		$sth->setFetchMode(PDO::FETCH_ASSOC);
+
+		try {
+			if (!$sth->execute($values))
+				throw new DatabaseException($this);
+		} catch (PDOException $e) {
+			throw new DatabaseException($sth);
+		}
+		return $sth;
+	}
+
+	/**
+	 * Execute a query that returns maximum of one row with one field and return it.
+	 *
+	 * @param string $sql Raw SQL string to execute.
+	 * @param array &$values Optional array of values to bind to the query.
+	 * @return string
+	 */
+	public function query_and_fetch_one($sql, &$values=array())
+	{
+		$sth = $this->query($sql, $values);
+		$row = $sth->fetch(PDO::FETCH_NUM);
+		return $row[0];
+	}
+
+	/**
+	 * Execute a raw SQL query and fetch the results.
+	 *
+	 * @param string $sql Raw SQL string to execute.
+	 * @param Closure $handler Closure that will be passed the fetched results.
+	 */
+	public function query_and_fetch($sql, Closure $handler)
+	{
+		$sth = $this->query($sql);
+
+		while (($row = $sth->fetch(PDO::FETCH_ASSOC)))
+			$handler($row);
+	}
+
+	/**
+	 * Returns all tables for the current database.
+	 *
+	 * @return array Array containing table names.
+	 */
+	public function tables()
+	{
+		$tables = array();
+		$sth = $this->query_for_tables();
+
+		while (($row = $sth->fetch(PDO::FETCH_NUM)))
+			$tables[] = $row[0];
+
+		return $tables;
+	}
+
+	/**
+	 * Starts a transaction.
+	 */
+	public function transaction()
+	{
+		if (!$this->connection->beginTransaction())
+			throw new DatabaseException($this);
+	}
+
+	/**
+	 * Commits the current transaction.
+	 */
+	public function commit()
+	{
+		if (!$this->connection->commit())
+			throw new DatabaseException($this);
+	}
+
+	/**
+	 * Rollback a transaction.
+	 */
+	public function rollback()
+	{
+		if (!$this->connection->rollback())
+			throw new DatabaseException($this);
+	}
+
+	/**
+	 * Tells you if this adapter supports sequences or not.
+	 *
+	 * @return boolean
+	 */
+	function supports_sequences()
+	{
+		return false;
+	}
+
+	/**
+	 * Return a default sequence name for the specified table.
+	 *
+	 * @param string $table Name of a table
+	 * @param string $column_name Name of column sequence is for
+	 * @return string sequence name or null if not supported.
+	 */
+	public function get_sequence_name($table, $column_name)
+	{
+		return "{$table}_seq";
+	}
+
+	/**
+	 * Return SQL for getting the next value in a sequence.
+	 *
+	 * @param string $sequence_name Name of the sequence
+	 * @return string
+	 */
+	public function next_sequence_value($sequence_name)
+	{
+		return null;
+	}
+
+	/**
+	 * Quote a name like table names and field names.
+	 *
+	 * @param string $string String to quote.
+	 * @return string
+	 */
+	public function quote_name($string)
+	{
+		return $string[0] === static::$QUOTE_CHARACTER || $string[strlen($string) - 1] === static::$QUOTE_CHARACTER ?
+			$string : static::$QUOTE_CHARACTER . $string . static::$QUOTE_CHARACTER;
+	}
+
+	/**
+	 * Return a date time formatted into the database's date format.
+	 *
+	 * @param DateTime $datetime The DateTime object
+	 * @return string
+	 */
+	public function date_to_string($datetime)
+	{
+		return $datetime->format('Y-m-d');
+	}
+
+	/**
+	 * Return a date time formatted into the database's datetime format.
+	 *
+	 * @param DateTime $datetime The DateTime object
+	 * @return string
+	 */
+	public function datetime_to_string($datetime)
+	{
+		return $datetime->format('Y-m-d H:i:s T');
+	}
+
+	/**
+	 * Converts a string representation of a datetime into a DateTime object.
+	 *
+	 * @param string $string A datetime in the form accepted by date_create()
+	 * @return DateTime
+	 */
+	public function string_to_datetime($string)
+	{
+		$date = date_create($string);
+		$errors = \DateTime::getLastErrors();
+
+		if ($errors['warning_count'] > 0 || $errors['error_count'] > 0)
+			return null;
+
+		return new DateTime($date->format('Y-m-d H:i:s T'));
+	}
+
+	/**
+	 * Adds a limit clause to the SQL query.
+	 *
+	 * @param string $sql The SQL statement.
+	 * @param int $offset Row offset to start at.
+	 * @param int $limit Maximum number of rows to return.
+	 * @return string The SQL query that will limit results to specified parameters
+	 */
+	abstract function limit($sql, $offset, $limit);
+
+	/**
+	 * Query for column meta info and return statement handle.
+	 *
+	 * @param string $table Name of a table
+	 * @return PDOStatement
+	 */
+	abstract public function query_column_info($table);
+
+	/**
+	 * Query for all tables in the current database. The result must only
+	 * contain one column which has the name of the table.
+	 *
+	 * @return PDOStatement
+	 */
+	abstract function query_for_tables();
+
+	/**
+	 * Executes query to specify the character set for this connection.
+	 */
+	abstract function set_encoding($charset);
+
+	/*
+	 * Returns an array mapping of native database types
+	 */
+
+	abstract public function native_database_types();
+
+	/**
+	 * Specifies whether or not adapter can use LIMIT/ORDER clauses with DELETE & UPDATE operations
+	 *
+	 * @internal
+	 * @returns boolean (FALSE by default)
+	 */
+	public function accepts_limit_and_order_for_update_and_delete()
+	{
+		return false;
+	}
+
+}
+
+;
+?>

+ 51 - 0
hhvm/php-activerecord/lib/ConnectionManager.php

@@ -0,0 +1,51 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Singleton to manage any and all database connections.
+ *
+ * @package ActiveRecord
+ */
+class ConnectionManager extends Singleton
+{
+	/**
+	 * Array of {@link Connection} objects.
+	 * @var array
+	 */
+	static private $connections = array();
+
+	/**
+	 * If $name is null then the default connection will be returned.
+	 *
+	 * @see Config
+	 * @param string $name Optional name of a connection
+	 * @return Connection
+	 */
+	public static function get_connection($name=null)
+	{
+		$config = Config::instance();
+		$name = $name ? $name : $config->get_default_connection();
+
+		if (!isset(self::$connections[$name]) || !self::$connections[$name]->connection)
+			self::$connections[$name] = Connection::instance($config->get_connection($name));
+
+		return self::$connections[$name];
+	}
+
+	/**
+	 * Drops the connection from the connection manager. Does not actually close it since there
+	 * is no close method in PDO.
+	 *
+	 * @param string $name Name of the connection to forget about
+	 */
+	public static function drop_connection($name=null)
+	{
+		if (isset(self::$connections[$name]))
+			unset(self::$connections[$name]);
+	}
+}
+
+?>

+ 151 - 0
hhvm/php-activerecord/lib/DateTime.php

@@ -0,0 +1,151 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * An extension of PHP's DateTime class to provide dirty flagging and easier formatting options.
+ *
+ * All date and datetime fields from your database will be created as instances of this class.
+ *
+ * Example of formatting and changing the default format:
+ *
+ * <code>
+ * $now = new ActiveRecord\DateTime('2010-01-02 03:04:05');
+ * ActiveRecord\DateTime::$DEFAULT_FORMAT = 'short';
+ *
+ * echo $now->format();         # 02 Jan 03:04
+ * echo $now->format('atom');   # 2010-01-02T03:04:05-05:00
+ * echo $now->format('Y-m-d');  # 2010-01-02
+ *
+ * # __toString() uses the default formatter
+ * echo (string)$now;           # 02 Jan 03:04
+ * </code>
+ *
+ * You can also add your own pre-defined friendly formatters:
+ *
+ * <code>
+ * ActiveRecord\DateTime::$FORMATS['awesome_format'] = 'H:i:s m/d/Y';
+ * echo $now->format('awesome_format')  # 03:04:05 01/02/2010
+ * </code>
+ *
+ * @package ActiveRecord
+ * @see http://php.net/manual/en/class.datetime.php
+ */
+class DateTime extends \DateTime
+{
+	/**
+	 * Default format used for format() and __toString()
+	 */
+	public static $DEFAULT_FORMAT = 'rfc2822';
+
+	/**
+	 * Pre-defined format strings.
+	 */
+	public static $FORMATS = array(
+		'db'      => 'Y-m-d H:i:s',
+		'number'  => 'YmdHis',
+		'time'    => 'H:i',
+		'short'   => 'd M H:i',
+		'long'    => 'F d, Y H:i',
+		'atom'    => \DateTime::ATOM,
+		'cookie'  => \DateTime::COOKIE,
+		'iso8601' => \DateTime::ISO8601,
+		'rfc822'  => \DateTime::RFC822,
+		'rfc850'  => \DateTime::RFC850,
+		'rfc1036' => \DateTime::RFC1036,
+		'rfc1123' => \DateTime::RFC1123,
+		'rfc2822' => \DateTime::RFC2822,
+		'rfc3339' => \DateTime::RFC3339,
+		'rss'     => \DateTime::RSS,
+		'w3c'     => \DateTime::W3C);
+
+	private $model;
+	private $attribute_name;
+
+	public function attribute_of($model, $attribute_name)
+	{
+		$this->model = $model;
+		$this->attribute_name = $attribute_name;
+	}
+
+	/**
+	 * Formats the DateTime to the specified format.
+	 *
+	 * <code>
+	 * $datetime->format();         # uses the format defined in DateTime::$DEFAULT_FORMAT
+	 * $datetime->format('short');  # d M H:i
+	 * $datetime->format('Y-m-d');  # Y-m-d
+	 * </code>
+	 *
+	 * @see FORMATS
+	 * @see get_format
+	 * @param string $format A format string accepted by get_format()
+	 * @return string formatted date and time string
+	 */
+	public function format($format=null)
+	{
+		return parent::format(self::get_format($format));
+	}
+
+	/**
+	 * Returns the format string.
+	 *
+	 * If $format is a pre-defined format in $FORMATS it will return that otherwise
+	 * it will assume $format is a format string itself.
+	 *
+	 * @see FORMATS
+	 * @param string $format A pre-defined string format or a raw format string
+	 * @return string a format string
+	 */
+	public static function get_format($format=null)
+	{
+		// use default format if no format specified
+		if (!$format)
+			$format = self::$DEFAULT_FORMAT;
+
+		// format is a friendly
+		if (array_key_exists($format, self::$FORMATS))
+			 return self::$FORMATS[$format];
+
+		// raw format
+		return $format;
+	}
+
+	public function __toString()
+	{
+		return $this->format();
+	}
+
+	private function flag_dirty()
+	{
+		if ($this->model)
+			$this->model->flag_dirty($this->attribute_name);
+	}
+
+	public function setDate($year, $month, $day)
+	{
+		$this->flag_dirty();
+		call_user_func_array(array($this,'parent::setDate'),func_get_args());
+	}
+
+	public function setISODate($year, $week , $day=null)
+	{
+		$this->flag_dirty();
+		call_user_func_array(array($this,'parent::setISODate'),func_get_args());
+	}
+
+	public function setTime($hour, $minute, $second=null)
+	{
+		$this->flag_dirty();
+		call_user_func_array(array($this,'parent::setTime'),func_get_args());
+	}
+
+	public function setTimestamp($unixtimestamp)
+	{
+		$this->flag_dirty();
+		call_user_func_array(array($this,'parent::setTimestamp'),func_get_args());
+	}
+}
+?>

+ 137 - 0
hhvm/php-activerecord/lib/Exceptions.php

@@ -0,0 +1,137 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Generic base exception for all ActiveRecord specific errors.
+ *
+ * @package ActiveRecord
+ */
+class ActiveRecordException extends \Exception {};
+
+/**
+ * Thrown when a record cannot be found.
+ *
+ * @package ActiveRecord
+ */
+class RecordNotFound extends ActiveRecordException {};
+
+/**
+ * Thrown when there was an error performing a database operation.
+ *
+ * The error will be specific to whatever database you are running.
+ *
+ * @package ActiveRecord
+ */
+class DatabaseException extends ActiveRecordException
+{
+	public function __construct($adapter_or_string_or_mystery)
+	{
+		if ($adapter_or_string_or_mystery instanceof Connection)
+		{
+			parent::__construct(
+				join(", ",$adapter_or_string_or_mystery->connection->errorInfo()),
+				intval($adapter_or_string_or_mystery->connection->errorCode()));
+		}
+		elseif ($adapter_or_string_or_mystery instanceof \PDOStatement)
+		{
+			parent::__construct(
+				join(", ",$adapter_or_string_or_mystery->errorInfo()),
+				intval($adapter_or_string_or_mystery->errorCode()));
+		}
+		else
+			parent::__construct($adapter_or_string_or_mystery);
+	}
+};
+
+/**
+ * Thrown by {@link Model}.
+ *
+ * @package ActiveRecord
+ */
+class ModelException extends ActiveRecordException {};
+
+/**
+ * Thrown by {@link Expressions}.
+ *
+ * @package ActiveRecord
+ */
+class ExpressionsException extends ActiveRecordException {};
+
+/**
+ * Thrown for configuration problems.
+ *
+ * @package ActiveRecord
+ */
+class ConfigException extends ActiveRecordException {};
+
+/**
+ * Thrown when attempting to access an invalid property on a {@link Model}.
+ *
+ * @package ActiveRecord
+ */
+class UndefinedPropertyException extends ModelException
+{
+	/**
+	 * Sets the exception message to show the undefined property's name.
+	 *
+	 * @param str $property_name name of undefined property
+	 * @return void
+	 */
+	public function __construct($class_name, $property_name)
+	{
+		if (is_array($property_name))
+		{
+			$this->message = implode("\r\n", $property_name);
+			return;
+		}
+
+		$this->message = "Undefined property: {$class_name}->{$property_name} in {$this->file} on line {$this->line}";
+		parent::__construct();
+	}
+};
+
+/**
+ * Thrown when attempting to perform a write operation on a {@link Model} that is in read-only mode.
+ *
+ * @package ActiveRecord
+ */
+class ReadOnlyException extends ModelException
+{
+	/**
+	 * Sets the exception message to show the undefined property's name.
+	 *
+	 * @param str $class_name name of the model that is read only
+	 * @param str $method_name name of method which attempted to modify the model
+	 * @return void
+	 */
+	public function __construct($class_name, $method_name)
+	{
+		$this->message = "{$class_name}::{$method_name}() cannot be invoked because this model is set to read only";
+		parent::__construct();
+	}
+};
+
+/**
+ * Thrown for validations exceptions.
+ *
+ * @package ActiveRecord
+ */
+class ValidationsArgumentError extends ActiveRecordException {};
+
+/**
+ * Thrown for relationship exceptions.
+ *
+ * @package ActiveRecord
+ */
+class RelationshipException extends ActiveRecordException {};
+
+/**
+ * Thrown for has many thru exceptions.
+ *
+ * @package ActiveRecord
+ */
+class HasManyThroughAssociationException extends RelationshipException {};
+?>

+ 185 - 0
hhvm/php-activerecord/lib/Expressions.php

@@ -0,0 +1,185 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Templating like class for building SQL statements.
+ *
+ * Examples:
+ * 'name = :name AND author = :author'
+ * 'id = IN(:ids)'
+ * 'id IN(:subselect)'
+ * 
+ * @package ActiveRecord
+ */
+class Expressions
+{
+	const ParameterMarker = '?';
+
+	private $expressions;
+	private $values = array();
+	private $connection;
+
+	public function __construct($connection, $expressions=null /* [, $values ... ] */)
+	{
+		$values = null;
+		$this->connection = $connection;
+
+		if (is_array($expressions))
+		{
+			$glue = func_num_args() > 2 ? func_get_arg(2) : ' AND ';
+			list($expressions,$values) = $this->build_sql_from_hash($expressions,$glue);
+		}
+
+		if ($expressions != '')
+		{
+			if (!$values)
+				$values = array_slice(func_get_args(),2);
+
+			$this->values = $values;
+			$this->expressions = $expressions;
+		}
+	}
+
+	/**
+	 * Bind a value to the specific one based index. There must be a bind marker
+	 * for each value bound or to_s() will throw an exception.
+	 */
+	public function bind($parameter_number, $value)
+	{
+		if ($parameter_number <= 0)
+			throw new ExpressionsException("Invalid parameter index: $parameter_number");
+
+		$this->values[$parameter_number-1] = $value;
+	}
+
+	public function bind_values($values)
+	{
+		$this->values = $values;
+	}
+
+	/**
+	 * Returns all the values currently bound.
+	 */
+	public function values()
+	{
+		return $this->values;
+	}
+
+	/**
+	 * Returns the connection object.
+	 */
+	public function get_connection()
+	{
+		return $this->connection;
+	}
+
+	/**
+	 * Sets the connection object. It is highly recommended to set this so we can
+	 * use the adapter's native escaping mechanism.
+	 *
+	 * @param string $connection a Connection instance
+	 */
+	public function set_connection($connection)
+	{
+		$this->connection = $connection;
+	}
+
+	public function to_s($substitute=false, &$options=null)
+	{
+		if (!$options) $options = array();
+		
+		$values = array_key_exists('values',$options) ? $options['values'] : $this->values;
+
+		$ret = "";
+		$replace = array();
+		$num_values = count($values);
+		$len = strlen($this->expressions);
+		$quotes = 0;
+
+		for ($i=0,$n=strlen($this->expressions),$j=0; $i<$n; ++$i)
+		{
+			$ch = $this->expressions[$i];
+
+			if ($ch == self::ParameterMarker)
+			{
+				if ($quotes % 2 == 0)
+				{
+					if ($j > $num_values-1)
+						throw new ExpressionsException("No bound parameter for index $j");
+
+					$ch = $this->substitute($values,$substitute,$i,$j++);
+				}
+			}
+			elseif ($ch == '\'' && $i > 0 && $this->expressions[$i-1] != '\\')
+				++$quotes;
+
+			$ret .= $ch;
+		}
+		return $ret;
+	}
+
+	private function build_sql_from_hash(&$hash, $glue)
+	{
+		$sql = $g = "";
+
+		foreach ($hash as $name => $value)
+		{
+			if ($this->connection)
+				$name = $this->connection->quote_name($name);
+
+			if (is_array($value))
+				$sql .= "$g$name IN(?)";
+			elseif (is_null($value))
+				$sql .= "$g$name IS ?";
+			else
+				$sql .= "$g$name=?";
+
+			$g = $glue;
+		}
+		return array($sql,array_values($hash));
+	}
+
+	private function substitute(&$values, $substitute, $pos, $parameter_index)
+	{
+		$value = $values[$parameter_index];
+
+		if (is_array($value))
+		{
+			if ($substitute)
+			{
+				$ret = '';
+
+				for ($i=0,$n=count($value); $i<$n; ++$i)
+					$ret .= ($i > 0 ? ',' : '') . $this->stringify_value($value[$i]);
+
+				return $ret;
+			}
+			return join(',',array_fill(0,count($value),self::ParameterMarker));
+		}
+
+		if ($substitute)
+			return $this->stringify_value($value);
+
+		return $this->expressions[$pos];
+	}
+
+	private function stringify_value($value)
+	{
+		if (is_null($value))
+			return "NULL";
+
+		return is_string($value) ? $this->quote_string($value) : $value;
+	}
+
+	private function quote_string($value)
+	{
+		if ($this->connection)
+			return $this->connection->escape($value);
+
+		return "'" . str_replace("'","''",$value) . "'";
+	}
+}
+?>

+ 120 - 0
hhvm/php-activerecord/lib/Inflector.php

@@ -0,0 +1,120 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * @package ActiveRecord
+ */
+abstract class Inflector
+{
+	/**
+	 * Get an instance of the {@link Inflector} class.
+	 *
+	 * @return object
+	 */
+	public static function instance()
+	{
+		return new StandardInflector();
+	}
+
+	/**
+	 * Turn a string into its camelized version.
+	 *
+	 * @param string $s string to convert
+	 * @return string
+	 */
+	public function camelize($s)
+	{
+		$s = preg_replace('/[_-]+/','_',trim($s));
+		$s = str_replace(' ', '_', $s);
+
+		$camelized = '';
+
+		for ($i=0,$n=strlen($s); $i<$n; ++$i)
+		{
+			if ($s[$i] == '_' && $i+1 < $n)
+				$camelized .= strtoupper($s[++$i]);
+			else
+				$camelized .= $s[$i];
+		}
+
+		$camelized = trim($camelized,' _');
+
+		if (strlen($camelized) > 0)
+			$camelized[0] = strtolower($camelized[0]);
+
+		return $camelized;
+	}
+
+	/**
+	 * Determines if a string contains all uppercase characters.
+	 *
+	 * @param string $s string to check
+	 * @return bool
+	 */
+	public static function is_upper($s)
+	{
+		return (strtoupper($s) === $s);
+	}
+
+	/**
+	 * Determines if a string contains all lowercase characters.
+	 *
+	 * @param string $s string to check
+	 * @return bool
+	 */
+	public static function is_lower($s)
+	{
+		return (strtolower($s) === $s);
+	}
+
+	/**
+	 * Convert a camelized string to a lowercase, underscored string.
+	 *
+	 * @param string $s string to convert
+	 * @return string
+	 */
+	public function uncamelize($s)
+	{
+		$normalized = '';
+
+		for ($i=0,$n=strlen($s); $i<$n; ++$i)
+		{
+			if (ctype_alpha($s[$i]) && self::is_upper($s[$i]))
+				$normalized .= '_' . strtolower($s[$i]);
+			else
+				$normalized .= $s[$i];
+		}
+		return trim($normalized,' _');
+	}
+
+	/**
+	 * Convert a string with space into a underscored equivalent.
+	 *
+	 * @param string $s string to convert
+	 * @return string
+	 */
+	public function underscorify($s)
+	{
+		return preg_replace(array('/[_\- ]+/','/([a-z])([A-Z])/'),array('_','\\1_\\2'),trim($s));
+	}
+
+	public function keyify($class_name)
+	{
+		return strtolower($this->underscorify(denamespace($class_name))) . '_id';
+	}
+
+	abstract function variablize($s);
+}
+
+/**
+ * @package ActiveRecord
+ */
+class StandardInflector extends Inflector
+{
+	public function tableize($s) { return Utils::pluralize(strtolower($this->underscorify($s))); }
+	public function variablize($s) { return str_replace(array('-',' '),array('_','_'),strtolower(trim($s))); }
+}
+?>

+ 1861 - 0
hhvm/php-activerecord/lib/Model.php

@@ -0,0 +1,1861 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * The base class for your models.
+ *
+ * Defining an ActiveRecord model for a table called people and orders:
+ *
+ * <code>
+ * CREATE TABLE people(
+ *   id int primary key auto_increment,
+ *   parent_id int,
+ *   first_name varchar(50),
+ *   last_name varchar(50)
+ * );
+ *
+ * CREATE TABLE orders(
+ *   id int primary key auto_increment,
+ *   person_id int not null,
+ *   cost decimal(10,2),
+ *   total decimal(10,2)
+ * );
+ * </code>
+ *
+ * <code>
+ * class Person extends ActiveRecord\Model {
+ *   static $belongs_to = array(
+ *     array('parent', 'foreign_key' => 'parent_id', 'class_name' => 'Person')
+ *   );
+ *
+ *   static $has_many = array(
+ *     array('children', 'foreign_key' => 'parent_id', 'class_name' => 'Person'),
+ *     array('orders')
+ *   );
+ *
+ *   static $validates_length_of = array(
+ *     array('first_name', 'within' => array(1,50)),
+ *     array('last_name', 'within' => array(1,50))
+ *   );
+ * }
+ *
+ * class Order extends ActiveRecord\Model {
+ *   static $belongs_to = array(
+ *     array('person')
+ *   );
+ *
+ *   static $validates_numericality_of = array(
+ *     array('cost', 'greater_than' => 0),
+ *     array('total', 'greater_than' => 0)
+ *   );
+ *
+ *   static $before_save = array('calculate_total_with_tax');
+ *
+ *   public function calculate_total_with_tax() {
+ *     $this->total = $this->cost * 0.045;
+ *   }
+ * }
+ * </code>
+ *
+ * For a more in-depth look at defining models, relationships, callbacks and many other things
+ * please consult our {@link http://www.phpactiverecord.org/guides Guides}.
+ *
+ * @package ActiveRecord
+ * @see BelongsTo
+ * @see CallBack
+ * @see HasMany
+ * @see HasAndBelongsToMany
+ * @see Serialization
+ * @see Validations
+ */
+class Model
+{
+	/**
+	 * An instance of {@link Errors} and will be instantiated once a write method is called.
+	 *
+	 * @var Errors
+	 */
+	public $errors;
+
+	/**
+	 * Contains model values as column_name => value
+	 *
+	 * @var array
+	 */
+	private $attributes = array();
+
+	/**
+	 * Flag whether or not this model's attributes have been modified since it will either be null or an array of column_names that have been modified
+	 *
+	 * @var array
+	 */
+	private $__dirty = null;
+
+	/**
+	 * Flag that determines of this model can have a writer method invoked such as: save/update/insert/delete
+	 *
+	 * @var boolean
+	 */
+	private $__readonly = false;
+
+	/**
+	 * Array of relationship objects as model_attribute_name => relationship
+	 *
+	 * @var array
+	 */
+	private $__relationships = array();
+
+	/**
+	 * Flag that determines if a call to save() should issue an insert or an update sql statement
+	 *
+	 * @var boolean
+	 */
+	private $__new_record = true;
+
+	/**
+	 * Set to the name of the connection this {@link Model} should use.
+	 *
+	 * @var string
+	 */
+	static $connection;
+
+	/**
+	 * Set to the name of the database this Model's table is in.
+	 *
+	 * @var string
+	 */
+	static $db;
+
+	/**
+	 * Set this to explicitly specify the model's table name if different from inferred name.
+	 *
+	 * If your table doesn't follow our table name convention you can set this to the
+	 * name of your table to explicitly tell ActiveRecord what your table is called.
+	 *
+	 * @var string
+	 */
+	static $table_name;
+
+	/**
+	 * Set this to override the default primary key name if different from default name of "id".
+	 *
+	 * @var string
+	 */
+	static $primary_key;
+
+	/**
+	 * Set this to explicitly specify the sequence name for the table.
+	 *
+	 * @var string
+	 */
+	static $sequence;
+
+	/**
+	 * Allows you to create aliases for attributes.
+	 *
+	 * <code>
+	 * class Person extends ActiveRecord\Model {
+	 *   static $alias_attribute = array(
+	 *     'alias_first_name' => 'first_name',
+	 *     'alias_last_name' => 'last_name');
+	 * }
+	 *
+	 * $person = Person::first();
+	 * $person->alias_first_name = 'Tito';
+	 * echo $person->alias_first_name;
+	 * </code>
+	 *
+	 * @var array
+	 */
+	static $alias_attribute = array();
+
+	/**
+	 * Whitelist of attributes that are checked from mass-assignment calls such as constructing a model or using update_attributes.
+	 *
+	 * This is the opposite of {@link attr_protected $attr_protected}.
+	 *
+	 * <code>
+	 * class Person extends ActiveRecord\Model {
+	 *   static $attr_accessible = array('first_name','last_name');
+	 * }
+	 *
+	 * $person = new Person(array(
+	 *   'first_name' => 'Tito',
+	 *   'last_name' => 'the Grief',
+	 *   'id' => 11111));
+	 *
+	 * echo $person->id; # => null
+	 * </code>
+	 *
+	 * @var array
+	 */
+	static $attr_accessible = array();
+
+	/**
+	 * Blacklist of attributes that cannot be mass-assigned.
+	 *
+	 * This is the opposite of {@link attr_accessible $attr_accessible} and the format
+	 * for defining these are exactly the same.
+	 *
+	 * If the attribute is both accessible and protected, it is treated as protected.
+	 *
+	 * @var array
+	 */
+	static $attr_protected = array();
+
+	/**
+	 * Delegates calls to a relationship.
+	 *
+	 * <code>
+	 * class Person extends ActiveRecord\Model {
+	 *   static $belongs_to = array(array('venue'),array('host'));
+	 *   static $delegate = array(
+	 *     array('name', 'state', 'to' => 'venue'),
+	 *     array('name', 'to' => 'host', 'prefix' => 'woot'));
+	 * }
+	 * </code>
+	 *
+	 * Can then do:
+	 *
+	 * <code>
+	 * $person->state     # same as calling $person->venue->state
+	 * $person->name      # same as calling $person->venue->name
+	 * $person->woot_name # same as calling $person->host->name
+	 * </code>
+	 *
+	 * @var array
+	 */
+	static $delegate = array();
+
+	/**
+	 * Constructs a model.
+	 *
+	 * When a user instantiates a new object (e.g.: it was not ActiveRecord that instantiated via a find)
+	 * then @var $attributes will be mapped according to the schema's defaults. Otherwise, the given
+	 * $attributes will be mapped via set_attributes_via_mass_assignment.
+	 *
+	 * <code>
+	 * new Person(array('first_name' => 'Tito', 'last_name' => 'the Grief'));
+	 * </code>
+	 *
+	 * @param array $attributes Hash containing names and values to mass assign to the model
+	 * @param boolean $guard_attributes Set to true to guard protected/non-accessible attributes
+	 * @param boolean $instantiating_via_find Set to true if this model is being created from a find call
+	 * @param boolean $new_record Set to true if this should be considered a new record
+	 * @return Model
+	 */
+	public function __construct(array $attributes=array(), $guard_attributes=true, $instantiating_via_find=false, $new_record=true)
+	{
+		$this->__new_record = $new_record;
+
+		// initialize attributes applying defaults
+		if (!$instantiating_via_find)
+		{
+			foreach (static::table()->columns as $name => $meta)
+				$this->attributes[$meta->inflected_name] = $meta->default;
+		}
+
+		$this->set_attributes_via_mass_assignment($attributes, $guard_attributes);
+
+		// since all attribute assignment now goes thru assign_attributes() we want to reset
+		// dirty if instantiating via find since nothing is really dirty when doing that
+		if ($instantiating_via_find)
+			$this->__dirty = array();
+
+		$this->invoke_callback('after_construct',false);
+	}
+
+	/**
+	 * Magic method which delegates to read_attribute(). This handles firing off getter methods,
+	 * as they are not checked/invoked inside of read_attribute(). This circumvents the problem with
+	 * a getter being accessed with the same name as an actual attribute.
+	 *
+	 * You can also define customer getter methods for the model.
+	 *
+	 * EXAMPLE:
+	 * <code>
+	 * class User extends ActiveRecord\Model {
+	 *
+	 *   # define custom getter methods. Note you must
+	 *   # prepend get_ to your method name:
+	 *   function get_middle_initial() {
+	 *     return $this->middle_name{0};
+	 *   }
+	 * }
+	 *
+	 * $user = new User();
+	 * echo $user->middle_name;  # will call $user->get_middle_name()
+	 * </code>
+	 *
+	 * If you define a custom getter with the same name as an attribute then you
+	 * will need to use read_attribute() to get the attribute's value.
+	 * This is necessary due to the way __get() works.
+	 *
+	 * For example, assume 'name' is a field on the table and we're defining a
+	 * custom getter for 'name':
+	 *
+	 * <code>
+	 * class User extends ActiveRecord\Model {
+	 *
+	 *   # INCORRECT way to do it
+	 *   # function get_name() {
+	 *   #   return strtoupper($this->name);
+	 *   # }
+	 *
+	 *   function get_name() {
+	 *     return strtoupper($this->read_attribute('name'));
+	 *   }
+	 * }
+	 *
+	 * $user = new User();
+	 * $user->name = 'bob';
+	 * echo $user->name; # => BOB
+	 * </code>
+	 *
+	 *
+	 * @see read_attribute()
+	 * @param string $name Name of an attribute
+	 * @return mixed The value of the attribute
+	 */
+	public function &__get($name)
+	{
+		// check for getter
+		if (method_exists($this, "get_$name"))
+		{
+			$name = "get_$name";
+			$value = $this->$name();
+			return $value;
+		}
+
+		return $this->read_attribute($name);
+	}
+
+	/**
+	 * Determines if an attribute exists for this {@link Model}.
+	 *
+	 * @param string $attribute_name
+	 * @return boolean
+	 */
+	public function __isset($attribute_name)
+	{
+		return array_key_exists($attribute_name,$this->attributes) || array_key_exists($attribute_name,static::$alias_attribute);
+	}
+
+	/**
+	 * Magic allows un-defined attributes to set via $attributes.
+	 *
+	 * You can also define customer setter methods for the model.
+	 *
+	 * EXAMPLE:
+	 * <code>
+	 * class User extends ActiveRecord\Model {
+	 *
+	 *   # define custom setter methods. Note you must
+	 *   # prepend set_ to your method name:
+	 *   function set_password($plaintext) {
+	 *     $this->encrypted_password = md5($plaintext);
+	 *   }
+	 * }
+	 *
+	 * $user = new User();
+	 * $user->password = 'plaintext';  # will call $user->set_password('plaintext')
+	 * </code>
+	 *
+	 * If you define a custom setter with the same name as an attribute then you
+	 * will need to use assign_attribute() to assign the value to the attribute.
+	 * This is necessary due to the way __set() works.
+	 *
+	 * For example, assume 'name' is a field on the table and we're defining a
+	 * custom setter for 'name':
+	 *
+	 * <code>
+	 * class User extends ActiveRecord\Model {
+	 *
+	 *   # INCORRECT way to do it
+	 *   # function set_name($name) {
+	 *   #   $this->name = strtoupper($name);
+	 *   # }
+	 *
+	 *   function set_name($name) {
+	 *     $this->assign_attribute('name',strtoupper($name));
+	 *   }
+	 * }
+	 *
+	 * $user = new User();
+	 * $user->name = 'bob';
+	 * echo $user->name; # => BOB
+	 * </code>
+	 *
+	 * @throws {@link UndefinedPropertyException} if $name does not exist
+	 * @param string $name Name of attribute, relationship or other to set
+	 * @param mixed $value The value
+	 * @return mixed The value
+	 */
+	public function __set($name, $value)
+	{
+		if (array_key_exists($name, static::$alias_attribute))
+			$name = static::$alias_attribute[$name];
+
+		elseif (method_exists($this,"set_$name"))
+		{
+			$name = "set_$name";
+			return $this->$name($value);
+		}
+
+		if (array_key_exists($name,$this->attributes))
+			return $this->assign_attribute($name,$value);
+
+		if ($name == 'id')
+			return $this->assign_attribute($this->get_primary_key(true),$value);
+
+		foreach (static::$delegate as &$item)
+		{
+			if (($delegated_name = $this->is_delegated($name,$item)))
+				return $this->$item['to']->$delegated_name = $value;
+		}
+
+		throw new UndefinedPropertyException(get_called_class(),$name);
+	}
+
+	public function __wakeup()
+	{
+		// make sure the models Table instance gets initialized when waking up
+		static::table();
+	}
+
+	/**
+	 * Assign a value to an attribute.
+	 *
+	 * @param string $name Name of the attribute
+	 * @param mixed &$value Value of the attribute
+	 * @return mixed the attribute value
+	 */
+	public function assign_attribute($name, $value)
+	{
+		$table = static::table();
+		if (!is_object($value)) {
+			if (array_key_exists($name, $table->columns)) {
+				$value = $table->columns[$name]->cast($value, static::connection());
+			} else {
+				$col = $table->get_column_by_inflected_name($name);
+				if (!is_null($col)){
+					$value = $col->cast($value, static::connection());
+				}
+			}
+		}
+
+		// convert php's \DateTime to ours
+		if ($value instanceof \DateTime)
+			$value = new DateTime($value->format('Y-m-d H:i:s T'));
+
+		// make sure DateTime values know what model they belong to so
+		// dirty stuff works when calling set methods on the DateTime object
+		if ($value instanceof DateTime)
+			$value->attribute_of($this,$name);
+
+		$this->attributes[$name] = $value;
+		$this->flag_dirty($name);
+		return $value;
+	}
+
+	/**
+	 * Retrieves an attribute's value or a relationship object based on the name passed. If the attribute
+	 * accessed is 'id' then it will return the model's primary key no matter what the actual attribute name is
+	 * for the primary key.
+	 *
+	 * @param string $name Name of an attribute
+	 * @return mixed The value of the attribute
+	 * @throws {@link UndefinedPropertyException} if name could not be resolved to an attribute, relationship, ...
+	 */
+	public function &read_attribute($name)
+	{
+		// check for aliased attribute
+		if (array_key_exists($name, static::$alias_attribute))
+			$name = static::$alias_attribute[$name];
+
+		// check for attribute
+		if (array_key_exists($name,$this->attributes))
+			return $this->attributes[$name];
+
+		// check relationships if no attribute
+		if (array_key_exists($name,$this->__relationships))
+			return $this->__relationships[$name];
+
+		$table = static::table();
+
+		// this may be first access to the relationship so check Table
+		if (($relationship = $table->get_relationship($name)))
+		{
+			$this->__relationships[$name] = $relationship->load($this);
+			return $this->__relationships[$name];
+		}
+
+		if ($name == 'id')
+		{
+			$pk = $this->get_primary_key(true);
+			if (isset($this->attributes[$pk]))
+				return $this->attributes[$pk];
+		}
+
+		//do not remove - have to return null by reference in strict mode
+		$null = null;
+
+		foreach (static::$delegate as &$item)
+		{
+			if (($delegated_name = $this->is_delegated($name,$item)))
+			{
+				$to = $item['to'];
+				if ($this->$to)
+				{
+					$val =& $this->$to->__get($delegated_name);
+					return $val;
+				}
+				else
+					return $null;
+			}
+		}
+
+		throw new UndefinedPropertyException(get_called_class(),$name);
+	}
+
+	/**
+	 * Flags an attribute as dirty.
+	 *
+	 * @param string $name Attribute name
+	 */
+	public function flag_dirty($name)
+	{
+		if (!$this->__dirty)
+			$this->__dirty = array();
+
+		$this->__dirty[$name] = true;
+	}
+
+	/**
+	 * Returns hash of attributes that have been modified since loading the model.
+	 *
+	 * @return mixed null if no dirty attributes otherwise returns array of dirty attributes.
+	 */
+	public function dirty_attributes()
+	{
+		if (!$this->__dirty)
+			return null;
+
+		$dirty = array_intersect_key($this->attributes,$this->__dirty);
+		return !empty($dirty) ? $dirty : null;
+	}
+
+	/**
+	 * Check if a particular attribute has been modified since loading the model.
+	 * @param string $attribute	Name of the attribute
+	 * @return boolean TRUE if it has been modified.
+	 */
+	public function attribute_is_dirty($attribute)
+	{
+		return $this->__dirty && $this->__dirty[$attribute] && array_key_exists($attribute, $this->attributes);
+	}
+
+	/**
+	 * Returns a copy of the model's attributes hash.
+	 *
+	 * @return array A copy of the model's attribute data
+	 */
+	public function attributes()
+	{
+		return $this->attributes;
+	}
+
+	/**
+	 * Retrieve the primary key name.
+	 *
+	 * @param boolean Set to true to return the first value in the pk array only
+	 * @return string The primary key for the model
+	 */
+	public function get_primary_key($first=false)
+	{
+		$pk = static::table()->pk;
+		return $first ? $pk[0] : $pk;
+	}
+
+	/**
+	 * Returns the actual attribute name if $name is aliased.
+	 *
+	 * @param string $name An attribute name
+	 * @return string
+	 */
+	public function get_real_attribute_name($name)
+	{
+		if (array_key_exists($name,$this->attributes))
+			return $name;
+
+		if (array_key_exists($name,static::$alias_attribute))
+			return static::$alias_attribute[$name];
+
+		return null;
+	}
+
+	/**
+	 * Returns array of validator data for this Model.
+	 *
+	 * Will return an array looking like:
+	 *
+	 * <code>
+	 * array(
+	 *   'name' => array(
+	 *     array('validator' => 'validates_presence_of'),
+	 *     array('validator' => 'validates_inclusion_of', 'in' => array('Bob','Joe','John')),
+	 *   'password' => array(
+	 *     array('validator' => 'validates_length_of', 'minimum' => 6))
+	 *   )
+	 * );
+	 * </code>
+	 *
+	 * @return array An array containing validator data for this model.
+	 */
+	public function get_validation_rules()
+	{
+		require_once 'Validations.php';
+
+		$validator = new Validations($this);
+		return $validator->rules();
+	}
+
+	/**
+	 * Returns an associative array containing values for all the attributes in $attributes
+	 *
+	 * @param array $attributes Array containing attribute names
+	 * @return array A hash containing $name => $value
+	 */
+	public function get_values_for($attributes)
+	{
+		$ret = array();
+
+		foreach ($attributes as $name)
+		{
+			if (array_key_exists($name,$this->attributes))
+				$ret[$name] = $this->attributes[$name];
+		}
+		return $ret;
+	}
+
+	/**
+	 * Retrieves the name of the table for this Model.
+	 *
+	 * @return string
+	 */
+	public static function table_name()
+	{
+		return static::table()->table;
+	}
+
+	/**
+	 * Returns the attribute name on the delegated relationship if $name is
+	 * delegated or null if not delegated.
+	 *
+	 * @param string $name Name of an attribute
+	 * @param array $delegate An array containing delegate data
+	 * @return delegated attribute name or null
+	 */
+	private function is_delegated($name, &$delegate)
+	{
+		if ($delegate['prefix'] != '')
+			$name = substr($name,strlen($delegate['prefix'])+1);
+
+		if (is_array($delegate) && in_array($name,$delegate['delegate']))
+			return $name;
+
+		return null;
+	}
+
+	/**
+	 * Determine if the model is in read-only mode.
+	 *
+	 * @return boolean
+	 */
+	public function is_readonly()
+	{
+		return $this->__readonly;
+	}
+
+	/**
+	 * Determine if the model is a new record.
+	 *
+	 * @return boolean
+	 */
+	public function is_new_record()
+	{
+		return $this->__new_record;
+	}
+
+	/**
+	 * Throws an exception if this model is set to readonly.
+	 *
+	 * @throws ActiveRecord\ReadOnlyException
+	 * @param string $method_name Name of method that was invoked on model for exception message
+	 */
+	private function verify_not_readonly($method_name)
+	{
+		if ($this->is_readonly())
+			throw new ReadOnlyException(get_class($this), $method_name);
+	}
+
+	/**
+	 * Flag model as readonly.
+	 *
+	 * @param boolean $readonly Set to true to put the model into readonly mode
+	 */
+	public function readonly($readonly=true)
+	{
+		$this->__readonly = $readonly;
+	}
+
+	/**
+	 * Retrieve the connection for this model.
+	 *
+	 * @return Connection
+	 */
+	public static function connection()
+	{
+		return static::table()->conn;
+	}
+
+	/**
+	 * Re-establishes the database connection with a new connection.
+	 *
+	 * @return Connection
+	 */
+	public static function reestablish_connection()
+	{
+		return static::table()->reestablish_connection();
+	}
+
+	/**
+	 * Returns the {@link Table} object for this model.
+	 *
+	 * Be sure to call in static scoping: static::table()
+	 *
+	 * @return Table
+	 */
+	public static function table()
+	{
+		return Table::load(get_called_class());
+	}
+
+	/**
+	 * Creates a model and saves it to the database.
+	 *
+	 * @param array $attributes Array of the models attributes
+	 * @param boolean $validate True if the validators should be run
+	 * @return Model
+	 */
+	public static function create($attributes, $validate=true)
+	{
+		$class_name = get_called_class();
+		$model = new $class_name($attributes);
+		$model->save($validate);
+		return $model;
+	}
+
+	/**
+	 * Save the model to the database.
+	 *
+	 * This function will automatically determine if an INSERT or UPDATE needs to occur.
+	 * If a validation or a callback for this model returns false, then the model will
+	 * not be saved and this will return false.
+	 *
+	 * If saving an existing model only data that has changed will be saved.
+	 *
+	 * @param boolean $validate Set to true or false depending on if you want the validators to run or not
+	 * @return boolean True if the model was saved to the database otherwise false
+	 */
+	public function save($validate=true)
+	{
+		$this->verify_not_readonly('save');
+		return $this->is_new_record() ? $this->insert($validate) : $this->update($validate);
+	}
+
+	/**
+	 * Issue an INSERT sql statement for this model's attribute.
+	 *
+	 * @see save
+	 * @param boolean $validate Set to true or false depending on if you want the validators to run or not
+	 * @return boolean True if the model was saved to the database otherwise false
+	 */
+	private function insert($validate=true)
+	{
+		$this->verify_not_readonly('insert');
+
+		if (($validate && !$this->_validate() || !$this->invoke_callback('before_create',false)))
+			return false;
+
+		$table = static::table();
+
+		if (!($attributes = $this->dirty_attributes()))
+			$attributes = $this->attributes;
+
+		$pk = $this->get_primary_key(true);
+		$use_sequence = false;
+
+		if ($table->sequence && !isset($attributes[$pk]))
+		{
+			if (($conn = static::connection()) instanceof OciAdapter)
+			{
+				// terrible oracle makes us select the nextval first
+				$attributes[$pk] = $conn->get_next_sequence_value($table->sequence);
+				$table->insert($attributes);
+				$this->attributes[$pk] = $attributes[$pk];
+			}
+			else
+			{
+				// unset pk that was set to null
+				if (array_key_exists($pk,$attributes))
+					unset($attributes[$pk]);
+
+				$table->insert($attributes,$pk,$table->sequence);
+				$use_sequence = true;
+			}
+		}
+		else
+			$table->insert($attributes);
+
+		// if we've got an autoincrementing/sequenced pk set it
+		// don't need this check until the day comes that we decide to support composite pks
+		// if (count($pk) == 1)
+		{
+			$column = $table->get_column_by_inflected_name($pk);
+
+			if ($column->auto_increment || $use_sequence)
+				$this->attributes[$pk] = static::connection()->insert_id($table->sequence);
+		}
+
+		$this->invoke_callback('after_create',false);
+		$this->__new_record = false;
+		return true;
+	}
+
+	/**
+	 * Issue an UPDATE sql statement for this model's dirty attributes.
+	 *
+	 * @see save
+	 * @param boolean $validate Set to true or false depending on if you want the validators to run or not
+	 * @return boolean True if the model was saved to the database otherwise false
+	 */
+	private function update($validate=true)
+	{
+		$this->verify_not_readonly('update');
+
+		if ($validate && !$this->_validate())
+			return false;
+
+		if ($this->is_dirty())
+		{
+			$pk = $this->values_for_pk();
+
+			if (empty($pk))
+				throw new ActiveRecordException("Cannot update, no primary key defined for: " . get_called_class());
+
+			if (!$this->invoke_callback('before_update',false))
+				return false;
+
+			$dirty = $this->dirty_attributes();
+			static::table()->update($dirty,$pk);
+			$this->invoke_callback('after_update',false);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Deletes records matching conditions in $options
+	 *
+	 * Does not instantiate models and therefore does not invoke callbacks
+	 *
+	 * Delete all using a hash:
+	 *
+	 * <code>
+	 * YourModel::delete_all(array('conditions' => array('name' => 'Tito')));
+	 * </code>
+	 *
+	 * Delete all using an array:
+	 *
+	 * <code>
+	 * YourModel::delete_all(array('conditions' => array('name = ?', 'Tito')));
+	 * </code>
+	 *
+	 * Delete all using a string:
+	 *
+	 * <code>
+	 * YourModel::delete_all(array('conditions' => 'name = "Tito"));
+	 * </code>
+	 *
+	 * An options array takes the following parameters:
+	 *
+	 * <ul>
+	 * <li><b>conditions:</b> Conditions using a string/hash/array</li>
+	 * <li><b>limit:</b> Limit number of records to delete (MySQL & Sqlite only)</li>
+	 * <li><b>order:</b> A SQL fragment for ordering such as: 'name asc', 'id desc, name asc' (MySQL & Sqlite only)</li>
+	 * </ul>
+	 *
+	 * @params array $options
+	 * return integer Number of rows affected
+	 */
+	public static function delete_all($options=array())
+	{
+		$table = static::table();
+		$conn = static::connection();
+		$sql = new SQLBuilder($conn, $table->get_fully_qualified_table_name());
+
+		$conditions = is_array($options) ? $options['conditions'] : $options;
+
+		if (is_array($conditions) && !is_hash($conditions))
+			call_user_func_array(array($sql, 'delete'), $conditions);
+		else
+			$sql->delete($conditions);
+
+		if (isset($options['limit']))
+			$sql->limit($options['limit']);
+
+		if (isset($options['order']))
+			$sql->order($options['order']);
+
+		$values = $sql->bind_values();
+		$ret = $conn->query(($table->last_sql = $sql->to_s()), $values);
+		return $ret->rowCount();
+	}
+
+	/**
+	 * Updates records using set in $options
+	 *
+	 * Does not instantiate models and therefore does not invoke callbacks
+	 *
+	 * Update all using a hash:
+	 *
+	 * <code>
+	 * YourModel::update_all(array('set' => array('name' => "Bob")));
+	 * </code>
+	 *
+	 * Update all using a string:
+	 *
+	 * <code>
+	 * YourModel::update_all(array('set' => 'name = "Bob"'));
+	 * </code>
+	 *
+	 * An options array takes the following parameters:
+	 *
+	 * <ul>
+	 * <li><b>set:</b> String/hash of field names and their values to be updated with
+	 * <li><b>conditions:</b> Conditions using a string/hash/array</li>
+	 * <li><b>limit:</b> Limit number of records to update (MySQL & Sqlite only)</li>
+	 * <li><b>order:</b> A SQL fragment for ordering such as: 'name asc', 'id desc, name asc' (MySQL & Sqlite only)</li>
+	 * </ul>
+	 *
+	 * @params array $options
+	 * return integer Number of rows affected
+	 */
+	public static function update_all($options=array())
+	{
+		$table = static::table();
+		$conn = static::connection();
+		$sql = new SQLBuilder($conn, $table->get_fully_qualified_table_name());
+
+		$sql->update($options['set']);
+
+		if (isset($options['conditions']) && ($conditions = $options['conditions']))
+		{
+			if (is_array($conditions) && !is_hash($conditions))
+				call_user_func_array(array($sql, 'where'), $conditions);
+			else
+				$sql->where($conditions);
+		}
+
+		if (isset($options['limit']))
+			$sql->limit($options['limit']);
+
+		if (isset($options['order']))
+			$sql->order($options['order']);
+
+		$values = $sql->bind_values();
+		$ret = $conn->query(($table->last_sql = $sql->to_s()), $values);
+		return $ret->rowCount();
+
+	}
+
+	/**
+	 * Deletes this model from the database and returns true if successful.
+	 *
+	 * @return boolean
+	 */
+	public function delete()
+	{
+		$this->verify_not_readonly('delete');
+
+		$pk = $this->values_for_pk();
+
+		if (empty($pk))
+			throw new ActiveRecordException("Cannot delete, no primary key defined for: " . get_called_class());
+
+		if (!$this->invoke_callback('before_destroy',false))
+			return false;
+
+		static::table()->delete($pk);
+		$this->invoke_callback('after_destroy',false);
+
+		return true;
+	}
+
+	/**
+	 * Helper that creates an array of values for the primary key(s).
+	 *
+	 * @return array An array in the form array(key_name => value, ...)
+	 */
+	public function values_for_pk()
+	{
+		return $this->values_for(static::table()->pk);
+	}
+
+	/**
+	 * Helper to return a hash of values for the specified attributes.
+	 *
+	 * @param array $attribute_names Array of attribute names
+	 * @return array An array in the form array(name => value, ...)
+	 */
+	public function values_for($attribute_names)
+	{
+		$filter = array();
+
+		foreach ($attribute_names as $name)
+			$filter[$name] = $this->$name;
+
+		return $filter;
+	}
+
+	/**
+	 * Validates the model.
+	 *
+	 * @return boolean True if passed validators otherwise false
+	 */
+	private function _validate()
+	{
+		require_once 'Validations.php';
+
+		$validator = new Validations($this);
+		$validation_on = 'validation_on_' . ($this->is_new_record() ? 'create' : 'update');
+
+		foreach (array('before_validation', "before_$validation_on") as $callback)
+		{
+			if (!$this->invoke_callback($callback,false))
+				return false;
+		}
+
+		// need to store reference b4 validating so that custom validators have access to add errors
+		$this->errors = $validator->get_record();
+		$validator->validate();
+
+		foreach (array('after_validation', "after_$validation_on") as $callback)
+			$this->invoke_callback($callback,false);
+
+		if (!$this->errors->is_empty())
+			return false;
+
+		return true;
+	}
+
+	/**
+	 * Returns true if the model has been modified.
+	 *
+	 * @return boolean true if modified
+	 */
+	public function is_dirty()
+	{
+		return empty($this->__dirty) ? false : true;
+	}
+
+	/**
+	 * Run validations on model and returns whether or not model passed validation.
+	 *
+	 * @see is_invalid
+	 * @return boolean
+	 */
+	public function is_valid()
+	{
+		return $this->_validate();
+	}
+
+	/**
+	 * Runs validations and returns true if invalid.
+	 *
+	 * @see is_valid
+	 * @return boolean
+	 */
+	public function is_invalid()
+	{
+		return !$this->_validate();
+	}
+
+	/**
+	 * Updates a model's timestamps.
+	 */
+	public function set_timestamps()
+	{
+		$now = date('Y-m-d H:i:s');
+
+		if (isset($this->updated_at))
+			$this->updated_at = $now;
+
+		if (isset($this->created_at) && $this->is_new_record())
+			$this->created_at = $now;
+	}
+
+	/**
+	 * Mass update the model with an array of attribute data and saves to the database.
+	 *
+	 * @param array $attributes An attribute data array in the form array(name => value, ...)
+	 * @return boolean True if successfully updated and saved otherwise false
+	 */
+	public function update_attributes($attributes)
+	{
+		$this->set_attributes($attributes);
+		return $this->save();
+	}
+
+	/**
+	 * Updates a single attribute and saves the record without going through the normal validation procedure.
+	 *
+	 * @param string $name Name of attribute
+	 * @param mixed $value Value of the attribute
+	 * @return boolean True if successful otherwise false
+	 */
+	public function update_attribute($name, $value)
+	{
+		$this->__set($name, $value);
+		return $this->update(false);
+	}
+
+	/**
+	 * Mass update the model with data from an attributes hash.
+	 *
+	 * Unlike update_attributes() this method only updates the model's data
+	 * but DOES NOT save it to the database.
+	 *
+	 * @see update_attributes
+	 * @param array $attributes An array containing data to update in the form array(name => value, ...)
+	 */
+	public function set_attributes(array $attributes)
+	{
+		$this->set_attributes_via_mass_assignment($attributes, true);
+	}
+
+	/**
+	 * Passing $guard_attributes as true will throw an exception if an attribute does not exist.
+	 *
+	 * @throws ActiveRecord\UndefinedPropertyException
+	 * @param array $attributes An array in the form array(name => value, ...)
+	 * @param boolean $guard_attributes Flag of whether or not protected/non-accessible attributes should be guarded
+	 */
+	private function set_attributes_via_mass_assignment(array &$attributes, $guard_attributes)
+	{
+		//access uninflected columns since that is what we would have in result set
+		$table = static::table();
+		$exceptions = array();
+		$use_attr_accessible = !empty(static::$attr_accessible);
+		$use_attr_protected = !empty(static::$attr_protected);
+		$connection = static::connection();
+
+		foreach ($attributes as $name => $value)
+		{
+			// is a normal field on the table
+			if (array_key_exists($name,$table->columns))
+			{
+				$value = $table->columns[$name]->cast($value,$connection);
+				$name = $table->columns[$name]->inflected_name;
+			}
+
+			if ($guard_attributes)
+			{
+				if ($use_attr_accessible && !in_array($name,static::$attr_accessible))
+					continue;
+
+				if ($use_attr_protected && in_array($name,static::$attr_protected))
+					continue;
+
+				// set valid table data
+				try {
+					$this->$name = $value;
+				} catch (UndefinedPropertyException $e) {
+					$exceptions[] = $e->getMessage();
+				}
+			}
+			else
+			{
+				// ignore OciAdapter's limit() stuff
+				if ($name == 'ar_rnum__')
+					continue;
+
+				// set arbitrary data
+				$this->assign_attribute($name,$value);
+			}
+		}
+
+		if (!empty($exceptions))
+			throw new UndefinedPropertyException(get_called_class(),$exceptions);
+	}
+
+	/**
+	 * Add a model to the given named ($name) relationship.
+	 *
+	 * @internal This should <strong>only</strong> be used by eager load
+	 * @param Model $model
+	 * @param $name of relationship for this table
+	 * @return void
+	 */
+	public function set_relationship_from_eager_load(Model $model=null, $name)
+	{
+		$table = static::table();
+
+		if (($rel = $table->get_relationship($name)))
+		{
+			if ($rel->is_poly())
+			{
+				// if the related model is null and it is a poly then we should have an empty array
+				if (is_null($model))
+					return $this->__relationships[$name] = array();
+				else
+					return $this->__relationships[$name][] = $model;
+			}
+			else
+				return $this->__relationships[$name] = $model;
+		}
+
+		throw new RelationshipException("Relationship named $name has not been declared for class: {$table->class->getName()}");
+	}
+
+	/**
+	 * Reloads the attributes and relationships of this object from the database.
+	 *
+	 * @return Model
+	 */
+	public function reload()
+	{
+		$this->__relationships = array();
+		$pk = array_values($this->get_values_for($this->get_primary_key()));
+
+		$this->set_attributes_via_mass_assignment($this->find($pk)->attributes, false);
+		$this->reset_dirty();
+
+		return $this;
+	}
+
+	public function __clone()
+	{
+		$this->__relationships = array();
+		$this->reset_dirty();
+		return $this;
+	}
+
+	/**
+	 * Resets the dirty array.
+	 *
+	 * @see dirty_attributes
+	 */
+	public function reset_dirty()
+	{
+		$this->__dirty = null;
+	}
+
+	/**
+	 * A list of valid finder options.
+	 *
+	 * @var array
+	 */
+	static $VALID_OPTIONS = array('conditions', 'limit', 'offset', 'order', 'select', 'joins', 'include', 'readonly', 'group', 'from', 'having');
+
+	/**
+	 * Enables the use of dynamic finders.
+	 *
+	 * Dynamic finders are just an easy way to do queries quickly without having to
+	 * specify an options array with conditions in it.
+	 *
+	 * <code>
+	 * SomeModel::find_by_first_name('Tito');
+	 * SomeModel::find_by_first_name_and_last_name('Tito','the Grief');
+	 * SomeModel::find_by_first_name_or_last_name('Tito','the Grief');
+	 * SomeModel::find_all_by_last_name('Smith');
+	 * SomeModel::count_by_name('Bob')
+	 * SomeModel::count_by_name_or_state('Bob','VA')
+	 * SomeModel::count_by_name_and_state('Bob','VA')
+	 * </code>
+	 *
+	 * You can also create the model if the find call returned no results:
+	 *
+	 * <code>
+	 * Person::find_or_create_by_name('Tito');
+	 *
+	 * # would be the equivalent of
+	 * if (!Person::find_by_name('Tito'))
+	 *   Person::create(array('Tito'));
+	 * </code>
+	 *
+	 * Some other examples of find_or_create_by:
+	 *
+	 * <code>
+	 * Person::find_or_create_by_name_and_id('Tito',1);
+	 * Person::find_or_create_by_name_and_id(array('name' => 'Tito', 'id' => 1));
+	 * </code>
+	 *
+	 * @param string $method Name of method
+	 * @param mixed $args Method args
+	 * @return Model
+	 * @throws {@link ActiveRecordException} if invalid query
+	 * @see find
+	 */
+	public static function __callStatic($method, $args)
+	{
+		$options = static::extract_and_validate_options($args);
+		$create = false;
+
+		if (substr($method,0,17) == 'find_or_create_by')
+		{
+			$attributes = substr($method,17);
+
+			// can't take any finders with OR in it when doing a find_or_create_by
+			if (strpos($attributes,'_or_') !== false)
+				throw new ActiveRecordException("Cannot use OR'd attributes in find_or_create_by");
+
+			$create = true;
+			$method = 'find_by' . substr($method,17);
+		}
+
+		if (substr($method,0,7) === 'find_by')
+		{
+			$attributes = substr($method,8);
+			$options['conditions'] = SQLBuilder::create_conditions_from_underscored_string(static::connection(),$attributes,$args,static::$alias_attribute);
+
+			if (!($ret = static::find('first',$options)) && $create)
+				return static::create(SQLBuilder::create_hash_from_underscored_string($attributes,$args,static::$alias_attribute));
+
+			return $ret;
+		}
+		elseif (substr($method,0,11) === 'find_all_by')
+		{
+			$options['conditions'] = SQLBuilder::create_conditions_from_underscored_string(static::connection(),substr($method,12),$args,static::$alias_attribute);
+			return static::find('all',$options);
+		}
+		elseif (substr($method,0,8) === 'count_by')
+		{
+			$options['conditions'] = SQLBuilder::create_conditions_from_underscored_string(static::connection(),substr($method,9),$args,static::$alias_attribute);
+			return static::count($options);
+		}
+
+		throw new ActiveRecordException("Call to undefined method: $method");
+	}
+
+	/**
+	 * Enables the use of build|create for associations.
+	 *
+	 * @param string $method Name of method
+	 * @param mixed $args Method args
+	 * @return mixed An instance of a given {@link AbstractRelationship}
+	 */
+	public function __call($method, $args)
+	{
+		//check for build|create_association methods
+		if (preg_match('/(build|create)_/', $method))
+		{
+			if (!empty($args))
+				$args = $args[0];
+
+			$association_name = str_replace(array('build_', 'create_'), '', $method);
+			$method = str_replace($association_name, 'association', $method);
+			$table = static::table();
+
+			if (($association = $table->get_relationship($association_name)) ||
+				  ($association = $table->get_relationship(($association_name = Utils::pluralize($association_name)))))
+			{
+				// access association to ensure that the relationship has been loaded
+				// so that we do not double-up on records if we append a newly created
+				$this->$association_name;
+				return $association->$method($this, $args);
+			}
+		}
+
+		throw new ActiveRecordException("Call to undefined method: $method");
+	}
+
+	/**
+	 * Alias for self::find('all').
+	 *
+	 * @see find
+	 * @return array array of records found
+	 */
+	public static function all(/* ... */)
+	{
+		return call_user_func_array('static::find',array_merge(array('all'),func_get_args()));
+	}
+
+	/**
+	 * Get a count of qualifying records.
+	 *
+	 * <code>
+	 * YourModel::count(array('conditions' => 'amount > 3.14159265'));
+	 * </code>
+	 *
+	 * @see find
+	 * @return int Number of records that matched the query
+	 */
+	public static function count(/* ... */)
+	{
+		$args = func_get_args();
+		$options = static::extract_and_validate_options($args);
+		$options['select'] = 'COUNT(*)';
+
+		if (!empty($args) && !is_null($args[0]) && !empty($args[0]))
+		{
+			if (is_hash($args[0]))
+				$options['conditions'] = $args[0];
+			else
+				$options['conditions'] = call_user_func_array('static::pk_conditions',$args);
+		}
+
+		$table = static::table();
+		$sql = $table->options_to_sql($options);
+		$values = $sql->get_where_values();
+		return static::connection()->query_and_fetch_one($sql->to_s(),$values);
+	}
+
+	/**
+	 * Determine if a record exists.
+	 *
+	 * <code>
+	 * SomeModel::exists(123);
+	 * SomeModel::exists(array('conditions' => array('id=? and name=?', 123, 'Tito')));
+	 * SomeModel::exists(array('id' => 123, 'name' => 'Tito'));
+	 * </code>
+	 *
+	 * @see find
+	 * @return boolean
+	 */
+	public static function exists(/* ... */)
+	{
+		return call_user_func_array('static::count',func_get_args()) > 0 ? true : false;
+	}
+
+	/**
+	 * Alias for self::find('first').
+	 *
+	 * @see find
+	 * @return Model The first matched record or null if not found
+	 */
+	public static function first(/* ... */)
+	{
+		return call_user_func_array('static::find',array_merge(array('first'),func_get_args()));
+	}
+
+	/**
+	 * Alias for self::find('last')
+	 *
+	 * @see find
+	 * @return Model The last matched record or null if not found
+	 */
+	public static function last(/* ... */)
+	{
+		return call_user_func_array('static::find',array_merge(array('last'),func_get_args()));
+	}
+
+	/**
+	 * Find records in the database.
+	 *
+	 * Finding by the primary key:
+	 *
+	 * <code>
+	 * # queries for the model with id=123
+	 * YourModel::find(123);
+	 *
+	 * # queries for model with id in(1,2,3)
+	 * YourModel::find(1,2,3);
+	 *
+	 * # finding by pk accepts an options array
+	 * YourModel::find(123,array('order' => 'name desc'));
+	 * </code>
+	 *
+	 * Finding by using a conditions array:
+	 *
+	 * <code>
+	 * YourModel::find('first', array('conditions' => array('name=?','Tito'),
+	 *   'order' => 'name asc'))
+	 * YourModel::find('all', array('conditions' => 'amount > 3.14159265'));
+	 * YourModel::find('all', array('conditions' => array('id in(?)', array(1,2,3))));
+	 * </code>
+	 *
+	 * Finding by using a hash:
+	 *
+	 * <code>
+	 * YourModel::find(array('name' => 'Tito', 'id' => 1));
+	 * YourModel::find('first',array('name' => 'Tito', 'id' => 1));
+	 * YourModel::find('all',array('name' => 'Tito', 'id' => 1));
+	 * </code>
+	 *
+	 * An options array can take the following parameters:
+	 *
+	 * <ul>
+	 * <li><b>select:</b> A SQL fragment for what fields to return such as: '*', 'people.*', 'first_name, last_name, id'</li>
+	 * <li><b>joins:</b> A SQL join fragment such as: 'JOIN roles ON(roles.user_id=user.id)' or a named association on the model</li>
+	 * <li><b>include:</b> TODO not implemented yet</li>
+	 * <li><b>conditions:</b> A SQL fragment such as: 'id=1', array('id=1'), array('name=? and id=?','Tito',1), array('name IN(?)', array('Tito','Bob')),
+	 * array('name' => 'Tito', 'id' => 1)</li>
+	 * <li><b>limit:</b> Number of records to limit the query to</li>
+	 * <li><b>offset:</b> The row offset to return results from for the query</li>
+	 * <li><b>order:</b> A SQL fragment for order such as: 'name asc', 'name asc, id desc'</li>
+	 * <li><b>readonly:</b> Return all the models in readonly mode</li>
+	 * <li><b>group:</b> A SQL group by fragment</li>
+	 * </ul>
+	 *
+	 * @throws {@link RecordNotFound} if no options are passed or finding by pk and no records matched
+	 * @return mixed An array of records found if doing a find_all otherwise a
+	 *   single Model object or null if it wasn't found. NULL is only return when
+	 *   doing a first/last find. If doing an all find and no records matched this
+	 *   will return an empty array.
+	 */
+	public static function find(/* $type, $options */)
+	{
+		$class = get_called_class();
+
+		if (func_num_args() <= 0)
+			throw new RecordNotFound("Couldn't find $class without an ID");
+
+		$args = func_get_args();
+		$options = static::extract_and_validate_options($args);
+		$num_args = count($args);
+		$single = true;
+
+		if ($num_args > 0 && ($args[0] === 'all' || $args[0] === 'first' || $args[0] === 'last'))
+		{
+			switch ($args[0])
+			{
+				case 'all':
+					$single = false;
+					break;
+
+			 	case 'last':
+					if (!array_key_exists('order',$options))
+						$options['order'] = join(' DESC, ',static::table()->pk) . ' DESC';
+					else
+						$options['order'] = SQLBuilder::reverse_order($options['order']);
+
+					// fall thru
+
+			 	case 'first':
+			 		$options['limit'] = 1;
+			 		$options['offset'] = 0;
+			 		break;
+			}
+
+			$args = array_slice($args,1);
+			$num_args--;
+		}
+		//find by pk
+		elseif (1 === count($args) && 1 == $num_args)
+			$args = $args[0];
+
+		// anything left in $args is a find by pk
+		if ($num_args > 0 && !isset($options['conditions']))
+			return static::find_by_pk($args, $options);
+
+		$options['mapped_names'] = static::$alias_attribute;
+		$list = static::table()->find($options);
+
+		return $single ? (!empty($list) ? $list[0] : null) : $list;
+	}
+
+	/**
+	 * Finder method which will find by a single or array of primary keys for this model.
+	 *
+	 * @see find
+	 * @param array $values An array containing values for the pk
+	 * @param array $options An options array
+	 * @return Model
+	 * @throws {@link RecordNotFound} if a record could not be found
+	 */
+	public static function find_by_pk($values, $options)
+	{
+		$options['conditions'] = static::pk_conditions($values);
+		$list = static::table()->find($options);
+		$results = count($list);
+
+		if ($results != ($expected = count($values)))
+		{
+			$class = get_called_class();
+
+			if ($expected == 1)
+			{
+				if (!is_array($values))
+					$values = array($values);
+
+				throw new RecordNotFound("Couldn't find $class with ID=" . join(',',$values));
+			}
+
+			$values = join(',',$values);
+			throw new RecordNotFound("Couldn't find all $class with IDs ($values) (found $results, but was looking for $expected)");
+		}
+		return $expected == 1 ? $list[0] : $list;
+	}
+
+	/**
+	 * Find using a raw SELECT query.
+	 *
+	 * <code>
+	 * YourModel::find_by_sql("SELECT * FROM people WHERE name=?",array('Tito'));
+	 * YourModel::find_by_sql("SELECT * FROM people WHERE name='Tito'");
+	 * </code>
+	 *
+	 * @param string $sql The raw SELECT query
+	 * @param array $values An array of values for any parameters that needs to be bound
+	 * @return array An array of models
+	 */
+	public static function find_by_sql($sql, $values=null)
+	{
+		return static::table()->find_by_sql($sql, $values, true);
+	}
+
+	/**
+	 * Helper method to run arbitrary queries against the model's database connection.
+	 *
+	 * @param string $sql SQL to execute
+	 * @param array $values Bind values, if any, for the query
+	 * @return object A PDOStatement object
+	 */
+	public static function query($sql, $values=null)
+	{
+		return static::connection()->query($sql, $values);
+	}
+
+	/**
+	 * Determines if the specified array is a valid ActiveRecord options array.
+	 *
+	 * @param array $array An options array
+	 * @param bool $throw True to throw an exception if not valid
+	 * @return boolean True if valid otherwise valse
+	 * @throws {@link ActiveRecordException} if the array contained any invalid options
+	 */
+	public static function is_options_hash($array, $throw=true)
+	{
+		if (is_hash($array))
+		{
+			$keys = array_keys($array);
+			$diff = array_diff($keys,self::$VALID_OPTIONS);
+
+			if (!empty($diff) && $throw)
+				throw new ActiveRecordException("Unknown key(s): " . join(', ',$diff));
+
+			$intersect = array_intersect($keys,self::$VALID_OPTIONS);
+
+			if (!empty($intersect))
+				return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Returns a hash containing the names => values of the primary key.
+	 *
+	 * @internal This needs to eventually support composite keys.
+	 * @param mixed $args Primary key value(s)
+	 * @return array An array in the form array(name => value, ...)
+	 */
+	public static function pk_conditions($args)
+	{
+		$table = static::table();
+		$ret = array($table->pk[0] => $args);
+		return $ret;
+	}
+
+	/**
+	 * Pulls out the options hash from $array if any.
+	 *
+	 * @internal DO NOT remove the reference on $array.
+	 * @param array &$array An array
+	 * @return array A valid options array
+	 */
+	public static function extract_and_validate_options(array &$array)
+	{
+		$options = array();
+
+		if ($array)
+		{
+			$last = &$array[count($array)-1];
+
+			try
+			{
+				if (self::is_options_hash($last))
+				{
+					array_pop($array);
+					$options = $last;
+				}
+			}
+			catch (ActiveRecordException $e)
+			{
+				if (!is_hash($last))
+					throw $e;
+
+				$options = array('conditions' => $last);
+			}
+		}
+		return $options;
+	}
+
+	/**
+	 * Returns a JSON representation of this model.
+	 *
+	 * @see Serialization
+	 * @param array $options An array containing options for json serialization (see {@link Serialization} for valid options)
+	 * @return string JSON representation of the model
+	 */
+	public function to_json(array $options=array())
+	{
+		return $this->serialize('Json', $options);
+	}
+
+	/**
+	 * Returns an XML representation of this model.
+	 *
+	 * @see Serialization
+	 * @param array $options An array containing options for xml serialization (see {@link Serialization} for valid options)
+	 * @return string XML representation of the model
+	 */
+	public function to_xml(array $options=array())
+	{
+		return $this->serialize('Xml', $options);
+	}
+
+   /**
+   * Returns an CSV representation of this model.
+   * Can take optional delimiter and enclosure
+   * (defaults are , and double quotes)
+   *
+   * Ex:
+   * <code>
+   * ActiveRecord\CsvSerializer::$delimiter=';';
+   * ActiveRecord\CsvSerializer::$enclosure='';
+   * YourModel::find('first')->to_csv(array('only'=>array('name','level')));
+   * returns: Joe,2
+   *
+   * YourModel::find('first')->to_csv(array('only_header'=>true,'only'=>array('name','level')));
+   * returns: name,level
+   * </code>
+   *
+   * @see Serialization
+   * @param array $options An array containing options for csv serialization (see {@link Serialization} for valid options)
+   * @return string CSV representation of the model
+   */
+  public function to_csv(array $options=array())
+  {
+    return $this->serialize('Csv', $options);
+  }
+
+	/**
+	 * Returns an Array representation of this model.
+	 *
+	 * @see Serialization
+	 * @param array $options An array containing options for json serialization (see {@link Serialization} for valid options)
+	 * @return array Array representation of the model
+	 */
+	public function to_array(array $options=array())
+	{
+		return $this->serialize('Array', $options);
+	}
+
+	/**
+	 * Creates a serializer based on pre-defined to_serializer()
+	 *
+	 * An options array can take the following parameters:
+	 *
+	 * <ul>
+	 * <li><b>only:</b> a string or array of attributes to be included.</li>
+	 * <li><b>excluded:</b> a string or array of attributes to be excluded.</li>
+	 * <li><b>methods:</b> a string or array of methods to invoke. The method's name will be used as a key for the final attributes array
+	 * along with the method's returned value</li>
+	 * <li><b>include:</b> a string or array of associated models to include in the final serialized product.</li>
+	 * </ul>
+	 *
+	 * @param string $type Either Xml, Json, Csv or Array
+	 * @param array $options Options array for the serializer
+	 * @return string Serialized representation of the model
+	 */
+	private function serialize($type, $options)
+	{
+		require_once 'Serialization.php';
+		$class = "ActiveRecord\\{$type}Serializer";
+		$serializer = new $class($this, $options);
+		return $serializer->to_s();
+	}
+
+	/**
+	 * Invokes the specified callback on this model.
+	 *
+	 * @param string $method_name Name of the call back to run.
+	 * @param boolean $must_exist Set to true to raise an exception if the callback does not exist.
+	 * @return boolean True if invoked or null if not
+	 */
+	private function invoke_callback($method_name, $must_exist=true)
+	{
+		return static::table()->callback->invoke($this,$method_name,$must_exist);
+	}
+
+	/**
+	 * Executes a block of code inside a database transaction.
+	 *
+	 * <code>
+	 * YourModel::transaction(function()
+	 * {
+	 *   YourModel::create(array("name" => "blah"));
+	 * });
+	 * </code>
+	 *
+	 * If an exception is thrown inside the closure the transaction will
+	 * automatically be rolled back. You can also return false from your
+	 * closure to cause a rollback:
+	 *
+	 * <code>
+	 * YourModel::transaction(function()
+	 * {
+	 *   YourModel::create(array("name" => "blah"));
+	 *   throw new Exception("rollback!");
+	 * });
+	 *
+	 * YourModel::transaction(function()
+	 * {
+	 *   YourModel::create(array("name" => "blah"));
+	 *   return false; # rollback!
+	 * });
+	 * </code>
+	 *
+	 * @param Closure $closure The closure to execute. To cause a rollback have your closure return false or throw an exception.
+	 * @return boolean True if the transaction was committed, False if rolled back.
+	 */
+	public static function transaction($closure)
+	{
+		$connection = static::connection();
+
+		try
+		{
+			$connection->transaction();
+
+			if ($closure() === false)
+			{
+				$connection->rollback();
+				return false;
+			}
+			else
+				$connection->commit();
+		}
+		catch (\Exception $e)
+		{
+			$connection->rollback();
+			throw $e;
+		}
+		return true;
+	}
+};
+?>

+ 86 - 0
hhvm/php-activerecord/lib/Reflections.php

@@ -0,0 +1,86 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+use ReflectionClass;
+
+/**
+ * Simple class that caches reflections of classes.
+ *
+ * @package ActiveRecord
+ */
+class Reflections extends Singleton
+{
+	/**
+	 * Current reflections.
+	 *
+	 * @var array
+	 */
+	private $reflections = array();
+
+	/**
+	 * Instantiates a new ReflectionClass for the given class.
+	 *
+	 * @param string $class Name of a class
+	 * @return Reflections $this so you can chain calls like Reflections::instance()->add('class')->get()
+	 */
+	public function add($class=null)
+	{
+		$class = $this->get_class($class);
+
+		if (!isset($this->reflections[$class]))
+			$this->reflections[$class] = new ReflectionClass($class);
+			
+		return $this;
+	}
+
+	/**
+	 * Destroys the cached ReflectionClass.
+	 *
+	 * Put this here mainly for testing purposes.
+	 * 
+	 * @param string $class Name of a class.
+	 * @return void
+	 */
+	public function destroy($class)
+	{
+		if (isset($this->reflections[$class]))
+			$this->reflections[$class] = null;
+	}
+	
+	/**
+	 * Get a cached ReflectionClass.
+	 *
+	 * @param string $class Optional name of a class
+	 * @return mixed null or a ReflectionClass instance
+	 * @throws ActiveRecordException if class was not found
+	 */
+	public function get($class=null)
+	{
+		$class = $this->get_class($class);
+
+		if (isset($this->reflections[$class]))
+			return $this->reflections[$class];
+
+		throw new ActiveRecordException("Class not found: $class");
+	}
+
+	/**
+	 * Retrieve a class name to be reflected.
+	 *
+	 * @param mixed $mixed An object or name of a class
+	 * @return string
+	 */
+	private function get_class($mixed=null)
+	{
+		if (is_object($mixed))
+			return get_class($mixed);
+
+		if (!is_null($mixed))
+			return $mixed;
+
+		return $this->get_called_class();
+	}
+}
+?>

+ 685 - 0
hhvm/php-activerecord/lib/Relationship.php

@@ -0,0 +1,685 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Interface for a table relationship.
+ *
+ * @package ActiveRecord
+ */
+interface InterfaceRelationship
+{
+	public function __construct($options=array());
+	public function build_association(Model $model, $attributes=array());
+	public function create_association(Model $model, $attributes=array());
+}
+
+/**
+ * Abstract class that all relationships must extend from.
+ *
+ * @package ActiveRecord
+ * @see http://www.phpactiverecord.org/guides/associations
+ */
+abstract class AbstractRelationship implements InterfaceRelationship
+{
+	/**
+	 * Name to be used that will trigger call to the relationship.
+	 *
+	 * @var string
+	 */
+	public $attribute_name;
+
+	/**
+	 * Class name of the associated model.
+	 *
+	 * @var string
+	 */
+	public $class_name;
+
+	/**
+	 * Name of the foreign key.
+	 *
+	 * @var string
+	 */
+	public $foreign_key = array();
+
+	/**
+	 * Options of the relationship.
+	 *
+	 * @var array
+	 */
+	protected $options = array();
+
+	/**
+	 * Is the relationship single or multi.
+	 *
+	 * @var boolean
+	 */
+	protected $poly_relationship = false;
+
+	/**
+	 * List of valid options for relationships.
+	 *
+	 * @var array
+	 */
+	static protected $valid_association_options = array('class_name', 'class', 'foreign_key', 'conditions', 'select', 'readonly', 'namespace');
+
+	/**
+	 * Constructs a relationship.
+	 *
+	 * @param array $options Options for the relationship (see {@link valid_association_options})
+	 * @return mixed
+	 */
+	public function __construct($options=array())
+	{
+		$this->attribute_name = $options[0];
+		$this->options = $this->merge_association_options($options);
+
+		$relationship = strtolower(denamespace(get_called_class()));
+
+		if ($relationship === 'hasmany' || $relationship === 'hasandbelongstomany')
+			$this->poly_relationship = true;
+
+		if (isset($this->options['conditions']) && !is_array($this->options['conditions']))
+			$this->options['conditions'] = array($this->options['conditions']);
+
+		if (isset($this->options['class']))
+			$this->set_class_name($this->options['class']);
+		elseif (isset($this->options['class_name']))
+			$this->set_class_name($this->options['class_name']);
+
+		$this->attribute_name = strtolower(Inflector::instance()->variablize($this->attribute_name));
+
+		if (!$this->foreign_key && isset($this->options['foreign_key']))
+			$this->foreign_key = is_array($this->options['foreign_key']) ? $this->options['foreign_key'] : array($this->options['foreign_key']);
+	}
+
+	protected function get_table()
+	{
+		return Table::load($this->class_name);
+	}
+
+	/**
+	 * What is this relationship's cardinality?
+	 *
+	 * @return bool
+	 */
+	public function is_poly()
+	{
+		return $this->poly_relationship;
+	}
+
+	/**
+	 * Eagerly loads relationships for $models.
+	 *
+	 * This method takes an array of models, collects PK or FK (whichever is needed for relationship), then queries
+	 * the related table by PK/FK and attaches the array of returned relationships to the appropriately named relationship on
+	 * $models.
+	 *
+	 * @param Table $table
+	 * @param $models array of model objects
+	 * @param $attributes array of attributes from $models
+	 * @param $includes array of eager load directives
+	 * @param $query_keys -> key(s) to be queried for on included/related table
+	 * @param $model_values_keys -> key(s)/value(s) to be used in query from model which is including
+	 * @return void
+	 */
+	protected function query_and_attach_related_models_eagerly(Table $table, $models, $attributes, $includes=array(), $query_keys=array(), $model_values_keys=array())
+	{
+		$values = array();
+		$options = $this->options;
+		$inflector = Inflector::instance();
+		$query_key = $query_keys[0];
+		$model_values_key = $model_values_keys[0];
+
+		foreach ($attributes as $column => $value)
+			$values[] = $value[$inflector->variablize($model_values_key)];
+
+		$values = array($values);
+		$conditions = SQLBuilder::create_conditions_from_underscored_string($table->conn,$query_key,$values);
+
+		if (isset($options['conditions']) && strlen($options['conditions'][0]) > 1)
+			Utils::add_condition($options['conditions'], $conditions);
+		else
+			$options['conditions'] = $conditions;
+
+		if (!empty($includes))
+			$options['include'] = $includes;
+
+		if (!empty($options['through'])) {
+			// save old keys as we will be reseting them below for inner join convenience
+			$pk = $this->primary_key;
+			$fk = $this->foreign_key;
+
+			$this->set_keys($this->get_table()->class->getName(), true);
+
+			if (!isset($options['class_name'])) {
+				$class = classify($options['through'], true);
+				if (isset($this->options['namespace']) && !class_exists($class))
+					$class = $this->options['namespace'].'\\'.$class;
+
+				$through_table = $class::table();
+			} else {
+				$class = $options['class_name'];
+				$relation = $class::table()->get_relationship($options['through']);
+				$through_table = $relation->get_table();
+			}
+			$options['joins'] = $this->construct_inner_join_sql($through_table, true);
+
+			$query_key = $this->primary_key[0];
+
+			// reset keys
+			$this->primary_key = $pk;
+			$this->foreign_key = $fk;
+		}
+
+		$options = $this->unset_non_finder_options($options);
+
+		$class = $this->class_name;
+
+		$related_models = $class::find('all', $options);
+		$used_models = array();
+		$model_values_key = $inflector->variablize($model_values_key);
+		$query_key = $inflector->variablize($query_key);
+
+		foreach ($models as $model)
+		{
+			$matches = 0;
+			$key_to_match = $model->$model_values_key;
+
+			foreach ($related_models as $related)
+			{
+				if ($related->$query_key == $key_to_match)
+				{
+					$hash = spl_object_hash($related);
+
+					if (in_array($hash, $used_models))
+						$model->set_relationship_from_eager_load(clone($related), $this->attribute_name);
+					else
+						$model->set_relationship_from_eager_load($related, $this->attribute_name);
+
+					$used_models[] = $hash;
+					$matches++;
+				}
+			}
+
+			if (0 === $matches)
+				$model->set_relationship_from_eager_load(null, $this->attribute_name);
+		}
+	}
+
+	/**
+	 * Creates a new instance of specified {@link Model} with the attributes pre-loaded.
+	 *
+	 * @param Model $model The model which holds this association
+	 * @param array $attributes Hash containing attributes to initialize the model with
+	 * @return Model
+	 */
+	public function build_association(Model $model, $attributes=array())
+	{
+		$class_name = $this->class_name;
+		return new $class_name($attributes);
+	}
+
+	/**
+	 * Creates a new instance of {@link Model} and invokes save.
+	 *
+	 * @param Model $model The model which holds this association
+	 * @param array $attributes Hash containing attributes to initialize the model with
+	 * @return Model
+	 */
+	public function create_association(Model $model, $attributes=array())
+	{
+		$class_name = $this->class_name;
+		$new_record = $class_name::create($attributes);
+		return $this->append_record_to_associate($model, $new_record);
+	}
+
+	protected function append_record_to_associate(Model $associate, Model $record)
+	{
+		$association =& $associate->{$this->attribute_name};
+
+		if ($this->poly_relationship)
+			$association[] = $record;
+		else
+			$association = $record;
+
+		return $record;
+	}
+
+	protected function merge_association_options($options)
+	{
+		$available_options = array_merge(self::$valid_association_options,static::$valid_association_options);
+		$valid_options = array_intersect_key(array_flip($available_options),$options);
+
+		foreach ($valid_options as $option => $v)
+			$valid_options[$option] = $options[$option];
+
+		return $valid_options;
+	}
+
+	protected function unset_non_finder_options($options)
+	{
+		foreach (array_keys($options) as $option)
+		{
+			if (!in_array($option, Model::$VALID_OPTIONS))
+				unset($options[$option]);
+		}
+		return $options;
+	}
+
+	/**
+	 * Infers the $this->class_name based on $this->attribute_name.
+	 *
+	 * Will try to guess the appropriate class by singularizing and uppercasing $this->attribute_name.
+	 *
+	 * @return void
+	 * @see attribute_name
+	 */
+	protected function set_inferred_class_name()
+	{
+		$singularize = ($this instanceOf HasMany ? true : false);
+		$this->set_class_name(classify($this->attribute_name, $singularize));
+	}
+
+	protected function set_class_name($class_name)
+	{
+		try {
+			$reflection = Reflections::instance()->add($class_name)->get($class_name);
+		} catch (\ReflectionException $e) {
+			if (isset($this->options['namespace'])) {
+				$class_name = $this->options['namespace'].'\\'.$class_name;
+				$reflection = Reflections::instance()->add($class_name)->get($class_name);
+			} else {
+				throw $e;
+			}
+		}
+
+		if (!$reflection->isSubClassOf('ActiveRecord\\Model'))
+			throw new RelationshipException("'$class_name' must extend from ActiveRecord\\Model");
+
+		$this->class_name = $class_name;
+	}
+
+	protected function create_conditions_from_keys(Model $model, $condition_keys=array(), $value_keys=array())
+	{
+		$condition_string = implode('_and_', $condition_keys);
+		$condition_values = array_values($model->get_values_for($value_keys));
+
+		// return null if all the foreign key values are null so that we don't try to do a query like "id is null"
+		if (all(null,$condition_values))
+			return null;
+
+		$conditions = SQLBuilder::create_conditions_from_underscored_string(Table::load(get_class($model))->conn,$condition_string,$condition_values);
+
+		# DO NOT CHANGE THE NEXT TWO LINES. add_condition operates on a reference and will screw options array up
+		if (isset($this->options['conditions']))
+			$options_conditions = $this->options['conditions'];
+		else
+			$options_conditions = array();
+
+		return Utils::add_condition($options_conditions, $conditions);
+	}
+
+	/**
+	 * Creates INNER JOIN SQL for associations.
+	 *
+	 * @param Table $from_table the table used for the FROM SQL statement
+	 * @param bool $using_through is this a THROUGH relationship?
+	 * @param string $alias a table alias for when a table is being joined twice
+	 * @return string SQL INNER JOIN fragment
+	 */
+	public function construct_inner_join_sql(Table $from_table, $using_through=false, $alias=null)
+	{
+		if ($using_through)
+		{
+			$join_table = $from_table;
+			$join_table_name = $from_table->get_fully_qualified_table_name();
+			$from_table_name = Table::load($this->class_name)->get_fully_qualified_table_name();
+ 		}
+		else
+		{
+			$join_table = Table::load($this->class_name);
+			$join_table_name = $join_table->get_fully_qualified_table_name();
+			$from_table_name = $from_table->get_fully_qualified_table_name();
+		}
+
+		// need to flip the logic when the key is on the other table
+		if ($this instanceof HasMany || $this instanceof HasOne)
+		{
+			$this->set_keys($from_table->class->getName());
+
+			if ($using_through)
+			{
+				$foreign_key = $this->primary_key[0];
+				$join_primary_key = $this->foreign_key[0];
+			}
+			else
+			{
+				$join_primary_key = $this->foreign_key[0];
+				$foreign_key = $this->primary_key[0];
+			}
+		}
+		else
+		{
+			$foreign_key = $this->foreign_key[0];
+			$join_primary_key = $this->primary_key[0];
+		}
+
+		if (!is_null($alias))
+		{
+			$aliased_join_table_name = $alias = $this->get_table()->conn->quote_name($alias);
+			$alias .= ' ';
+		}
+		else
+			$aliased_join_table_name = $join_table_name;
+
+		return "INNER JOIN $join_table_name {$alias}ON($from_table_name.$foreign_key = $aliased_join_table_name.$join_primary_key)";
+	}
+
+	/**
+	 * This will load the related model data.
+	 *
+	 * @param Model $model The model this relationship belongs to
+	 */
+	abstract function load(Model $model);
+};
+
+/**
+ * One-to-many relationship.
+ *
+ * <code>
+ * # Table: people
+ * # Primary key: id
+ * # Foreign key: school_id
+ * class Person extends ActiveRecord\Model {}
+ *
+ * # Table: schools
+ * # Primary key: id
+ * class School extends ActiveRecord\Model {
+ *   static $has_many = array(
+ *     array('people')
+ *   );
+ * });
+ * </code>
+ *
+ * Example using options:
+ *
+ * <code>
+ * class Payment extends ActiveRecord\Model {
+ *   static $belongs_to = array(
+ *     array('person'),
+ *     array('order')
+ *   );
+ * }
+ *
+ * class Order extends ActiveRecord\Model {
+ *   static $has_many = array(
+ *     array('people',
+ *           'through'    => 'payments',
+ *           'select'     => 'people.*, payments.amount',
+ *           'conditions' => 'payments.amount < 200')
+ *     );
+ * }
+ * </code>
+ *
+ * @package ActiveRecord
+ * @see http://www.phpactiverecord.org/guides/associations
+ * @see valid_association_options
+ */
+class HasMany extends AbstractRelationship
+{
+	/**
+	 * Valid options to use for a {@link HasMany} relationship.
+	 *
+	 * <ul>
+	 * <li><b>limit/offset:</b> limit the number of records</li>
+	 * <li><b>primary_key:</b> name of the primary_key of the association (defaults to "id")</li>
+	 * <li><b>group:</b> GROUP BY clause</li>
+	 * <li><b>order:</b> ORDER BY clause</li>
+	 * <li><b>through:</b> name of a model</li>
+	 * </ul>
+	 *
+	 * @var array
+	 */
+	static protected $valid_association_options = array('primary_key', 'order', 'group', 'having', 'limit', 'offset', 'through', 'source');
+
+	protected $primary_key;
+
+	private $has_one = false;
+	private $through;
+
+	/**
+	 * Constructs a {@link HasMany} relationship.
+	 *
+	 * @param array $options Options for the association
+	 * @return HasMany
+	 */
+	public function __construct($options=array())
+	{
+		parent::__construct($options);
+
+		if (isset($this->options['through']))
+		{
+			$this->through = $this->options['through'];
+
+			if (isset($this->options['source']))
+				$this->set_class_name($this->options['source']);
+		}
+
+		if (!$this->primary_key && isset($this->options['primary_key']))
+			$this->primary_key = is_array($this->options['primary_key']) ? $this->options['primary_key'] : array($this->options['primary_key']);
+
+		if (!$this->class_name)
+			$this->set_inferred_class_name();
+	}
+
+	protected function set_keys($model_class_name, $override=false)
+	{
+		//infer from class_name
+		if (!$this->foreign_key || $override)
+			$this->foreign_key = array(Inflector::instance()->keyify($model_class_name));
+
+		if (!$this->primary_key || $override)
+			$this->primary_key = Table::load($model_class_name)->pk;
+	}
+
+	public function load(Model $model)
+	{
+		$class_name = $this->class_name;
+		$this->set_keys(get_class($model));
+
+		// since through relationships depend on other relationships we can't do
+		// this initiailization in the constructor since the other relationship
+		// may not have been created yet and we only want this to run once
+		if (!isset($this->initialized))
+		{
+			if ($this->through)
+			{
+				// verify through is a belongs_to or has_many for access of keys
+				if (!($through_relationship = $this->get_table()->get_relationship($this->through)))
+					throw new HasManyThroughAssociationException("Could not find the association $this->through in model " . get_class($model));
+
+				if (!($through_relationship instanceof HasMany) && !($through_relationship instanceof BelongsTo))
+					throw new HasManyThroughAssociationException('has_many through can only use a belongs_to or has_many association');
+
+				// save old keys as we will be reseting them below for inner join convenience
+				$pk = $this->primary_key;
+				$fk = $this->foreign_key;
+
+				$this->set_keys($this->get_table()->class->getName(), true);
+				
+				$class = $this->class_name;
+				$relation = $class::table()->get_relationship($this->through);
+				$through_table = $relation->get_table();
+				$this->options['joins'] = $this->construct_inner_join_sql($through_table, true);
+
+				// reset keys
+				$this->primary_key = $pk;
+				$this->foreign_key = $fk;
+			}
+
+			$this->initialized = true;
+		}
+
+		if (!($conditions = $this->create_conditions_from_keys($model, $this->foreign_key, $this->primary_key)))
+			return null;
+
+		$options = $this->unset_non_finder_options($this->options);
+		$options['conditions'] = $conditions;
+		return $class_name::find($this->poly_relationship ? 'all' : 'first',$options);
+	}
+
+	private function inject_foreign_key_for_new_association(Model $model, &$attributes)
+	{
+		$this->set_keys($model);
+		$primary_key = Inflector::instance()->variablize($this->foreign_key[0]);
+
+		if (!isset($attributes[$primary_key]))
+			$attributes[$primary_key] = $model->id;
+
+		return $attributes;
+	}
+
+	public function build_association(Model $model, $attributes=array())
+	{
+		$attributes = $this->inject_foreign_key_for_new_association($model, $attributes);
+		return parent::build_association($model, $attributes);
+	}
+
+	public function create_association(Model $model, $attributes=array())
+	{
+		$attributes = $this->inject_foreign_key_for_new_association($model, $attributes);
+		return parent::create_association($model, $attributes);
+	}
+
+	public function load_eagerly($models=array(), $attributes=array(), $includes, Table $table)
+	{
+		$this->set_keys($table->class->name);
+		$this->query_and_attach_related_models_eagerly($table,$models,$attributes,$includes,$this->foreign_key, $table->pk);
+	}
+};
+
+/**
+ * One-to-one relationship.
+ *
+ * <code>
+ * # Table name: states
+ * # Primary key: id
+ * class State extends ActiveRecord\Model {}
+ *
+ * # Table name: people
+ * # Foreign key: state_id
+ * class Person extends ActiveRecord\Model {
+ *   static $has_one = array(array('state'));
+ * }
+ * </code>
+ *
+ * @package ActiveRecord
+ * @see http://www.phpactiverecord.org/guides/associations
+ */
+class HasOne extends HasMany
+{
+};
+
+/**
+ * @todo implement me
+ * @package ActiveRecord
+ * @see http://www.phpactiverecord.org/guides/associations
+ */
+class HasAndBelongsToMany extends AbstractRelationship
+{
+	public function __construct($options=array())
+	{
+		/* options =>
+		 *   join_table - name of the join table if not in lexical order
+		 *   foreign_key -
+		 *   association_foreign_key - default is {assoc_class}_id
+		 *   uniq - if true duplicate assoc objects will be ignored
+		 *   validate
+		 */
+	}
+
+	public function load(Model $model)
+	{
+
+	}
+};
+
+/**
+ * Belongs to relationship.
+ *
+ * <code>
+ * class School extends ActiveRecord\Model {}
+ *
+ * class Person extends ActiveRecord\Model {
+ *   static $belongs_to = array(
+ *     array('school')
+ *   );
+ * }
+ * </code>
+ *
+ * Example using options:
+ *
+ * <code>
+ * class School extends ActiveRecord\Model {}
+ *
+ * class Person extends ActiveRecord\Model {
+ *   static $belongs_to = array(
+ *     array('school', 'primary_key' => 'school_id')
+ *   );
+ * }
+ * </code>
+ *
+ * @package ActiveRecord
+ * @see valid_association_options
+ * @see http://www.phpactiverecord.org/guides/associations
+ */
+class BelongsTo extends AbstractRelationship
+{
+	public function __construct($options=array())
+	{
+		parent::__construct($options);
+
+		if (!$this->class_name)
+			$this->set_inferred_class_name();
+
+		//infer from class_name
+		if (!$this->foreign_key)
+			$this->foreign_key = array(Inflector::instance()->keyify($this->class_name));
+	}
+
+	public function __get($name)
+	{
+		if($name === 'primary_key' && !isset($this->primary_key)) {
+			$this->primary_key = array(Table::load($this->class_name)->pk[0]);
+		}
+
+		return $this->$name;
+	}
+
+	public function load(Model $model)
+	{
+		$keys = array();
+		$inflector = Inflector::instance();
+
+		foreach ($this->foreign_key as $key)
+			$keys[] = $inflector->variablize($key);
+
+		if (!($conditions = $this->create_conditions_from_keys($model, $this->primary_key, $keys)))
+			return null;
+
+		$options = $this->unset_non_finder_options($this->options);
+		$options['conditions'] = $conditions;
+		$class = $this->class_name;
+		return $class::first($options);
+	}
+
+	public function load_eagerly($models=array(), $attributes, $includes, Table $table)
+	{
+		$this->query_and_attach_related_models_eagerly($table,$models,$attributes,$includes, $this->primary_key,$this->foreign_key);
+	}
+};
+?>

+ 423 - 0
hhvm/php-activerecord/lib/SQLBuilder.php

@@ -0,0 +1,423 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Helper class for building sql statements progmatically.
+ *
+ * @package ActiveRecord
+ */
+class SQLBuilder
+{
+	private $connection;
+	private $operation = 'SELECT';
+	private $table;
+	private $select = '*';
+	private $joins;
+	private $order;
+	private $limit;
+	private $offset;
+	private $group;
+	private $having;
+	private $update;
+
+	// for where
+	private $where;
+	private $where_values = array();
+
+	// for insert/update
+	private $data;
+	private $sequence;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param Connection $connection A database connection object
+	 * @param string $table Name of a table
+	 * @return SQLBuilder
+	 * @throws ActiveRecordException if connection was invalid
+	 */
+	public function __construct($connection, $table)
+	{
+		if (!$connection)
+			throw new ActiveRecordException('A valid database connection is required.');
+
+		$this->connection	= $connection;
+		$this->table		= $table;
+	}
+
+	/**
+	 * Returns the SQL string.
+	 *
+	 * @return string
+	 */
+	public function __toString()
+	{
+		return $this->to_s();
+	}
+
+	/**
+	 * Returns the SQL string.
+	 *
+	 * @see __toString
+	 * @return string
+	 */
+	public function to_s()
+	{
+		$func = 'build_' . strtolower($this->operation);
+		return $this->$func();
+	}
+
+	/**
+	 * Returns the bind values.
+	 *
+	 * @return array
+	 */
+	public function bind_values()
+	{
+		$ret = array();
+
+		if ($this->data)
+			$ret = array_values($this->data);
+
+		if ($this->get_where_values())
+			$ret = array_merge($ret,$this->get_where_values());
+
+		return array_flatten($ret);
+	}
+
+	public function get_where_values()
+	{
+		return $this->where_values;
+	}
+
+	public function where(/* (conditions, values) || (hash) */)
+	{
+		$this->apply_where_conditions(func_get_args());
+		return $this;
+	}
+
+	public function order($order)
+	{
+		$this->order = $order;
+		return $this;
+	}
+
+	public function group($group)
+	{
+		$this->group = $group;
+		return $this;
+	}
+
+	public function having($having)
+	{
+		$this->having = $having;
+		return $this;
+	}
+
+	public function limit($limit)
+	{
+		$this->limit = intval($limit);
+		return $this;
+	}
+
+	public function offset($offset)
+	{
+		$this->offset = intval($offset);
+		return $this;
+	}
+
+	public function select($select)
+	{
+		$this->operation = 'SELECT';
+		$this->select = $select;
+		return $this;
+	}
+
+	public function joins($joins)
+	{
+		$this->joins = $joins;
+		return $this;
+	}
+
+	public function insert($hash, $pk=null, $sequence_name=null)
+	{
+		if (!is_hash($hash))
+			throw new ActiveRecordException('Inserting requires a hash.');
+
+		$this->operation = 'INSERT';
+		$this->data = $hash;
+
+		if ($pk && $sequence_name)
+			$this->sequence = array($pk,$sequence_name);
+
+		return $this;
+	}
+
+	public function update($mixed)
+	{
+		$this->operation = 'UPDATE';
+
+		if (is_hash($mixed))
+			$this->data = $mixed;
+		elseif (is_string($mixed))
+			$this->update = $mixed;
+		else
+			throw new ActiveRecordException('Updating requires a hash or string.');
+
+		return $this;
+	}
+
+	public function delete()
+	{
+		$this->operation = 'DELETE';
+		$this->apply_where_conditions(func_get_args());
+		return $this;
+	}
+
+	/**
+	 * Reverses an order clause.
+	 */
+	public static function reverse_order($order)
+	{
+		if (!trim($order))
+			return $order;
+
+		$parts = explode(',',$order);
+
+		for ($i=0,$n=count($parts); $i<$n; ++$i)
+		{
+			$v = strtolower($parts[$i]);
+
+			if (strpos($v,' asc') !== false)
+				$parts[$i] = preg_replace('/asc/i','DESC',$parts[$i]);
+			elseif (strpos($v,' desc') !== false)
+				$parts[$i] = preg_replace('/desc/i','ASC',$parts[$i]);
+			else
+				$parts[$i] .= ' DESC';
+		}
+		return join(',',$parts);
+	}
+
+	/**
+	 * Converts a string like "id_and_name_or_z" into a conditions value like array("id=? AND name=? OR z=?", values, ...).
+	 *
+	 * @param Connection $connection
+	 * @param $name Underscored string
+	 * @param $values Array of values for the field names. This is used
+	 *   to determine what kind of bind marker to use: =?, IN(?), IS NULL
+	 * @param $map A hash of "mapped_column_name" => "real_column_name"
+	 * @return A conditions array in the form array(sql_string, value1, value2,...)
+	 */
+	public static function create_conditions_from_underscored_string(Connection $connection, $name, &$values=array(), &$map=null)
+	{
+		if (!$name)
+			return null;
+
+		$parts = preg_split('/(_and_|_or_)/i',$name,-1,PREG_SPLIT_DELIM_CAPTURE);
+		$num_values = count($values);
+		$conditions = array('');
+
+		for ($i=0,$j=0,$n=count($parts); $i<$n; $i+=2,++$j)
+		{
+			if ($i >= 2)
+				$conditions[0] .= preg_replace(array('/_and_/i','/_or_/i'),array(' AND ',' OR '),$parts[$i-1]);
+
+			if ($j < $num_values)
+			{
+				if (!is_null($values[$j]))
+				{
+					$bind = is_array($values[$j]) ? ' IN(?)' : '=?';
+					$conditions[] = $values[$j];
+				}
+				else
+					$bind = ' IS NULL';
+			}
+			else
+				$bind = ' IS NULL';
+
+			// map to correct name if $map was supplied
+			$name = $map && isset($map[$parts[$i]]) ? $map[$parts[$i]] : $parts[$i];
+
+			$conditions[0] .= $connection->quote_name($name) . $bind;
+		}
+		return $conditions;
+	}
+
+	/**
+	 * Like create_conditions_from_underscored_string but returns a hash of name => value array instead.
+	 *
+	 * @param string $name A string containing attribute names connected with _and_ or _or_
+	 * @param $args Array of values for each attribute in $name
+	 * @param $map A hash of "mapped_column_name" => "real_column_name"
+	 * @return array A hash of array(name => value, ...)
+	 */
+	public static function create_hash_from_underscored_string($name, &$values=array(), &$map=null)
+	{
+		$parts = preg_split('/(_and_|_or_)/i',$name);
+		$hash = array();
+
+		for ($i=0,$n=count($parts); $i<$n; ++$i)
+		{
+			// map to correct name if $map was supplied
+			$name = $map && isset($map[$parts[$i]]) ? $map[$parts[$i]] : $parts[$i];
+			$hash[$name] = $values[$i];
+		}
+		return $hash;
+	}
+
+	/**
+	 * prepends table name to hash of field names to get around ambiguous fields when SQL builder
+	 * has joins
+	 *
+	 * @param array $hash
+	 * @return array $new
+	 */
+	private function prepend_table_name_to_fields($hash=array())
+	{
+		$new = array();
+		$table = $this->connection->quote_name($this->table);
+
+		foreach ($hash as $key => $value)
+		{
+			$k = $this->connection->quote_name($key);
+			$new[$table.'.'.$k] = $value;
+		}
+
+		return $new;
+	}
+
+	private function apply_where_conditions($args)
+	{
+		require_once 'Expressions.php';
+		$num_args = count($args);
+
+		if ($num_args == 1 && is_hash($args[0]))
+		{
+			$hash = is_null($this->joins) ? $args[0] : $this->prepend_table_name_to_fields($args[0]);
+			$e = new Expressions($this->connection,$hash);
+			$this->where = $e->to_s();
+			$this->where_values = array_flatten($e->values());
+		}
+		elseif ($num_args > 0)
+		{
+			// if the values has a nested array then we'll need to use Expressions to expand the bind marker for us
+			$values = array_slice($args,1);
+
+			foreach ($values as $name => &$value)
+			{
+				if (is_array($value))
+				{
+					$e = new Expressions($this->connection,$args[0]);
+					$e->bind_values($values);
+					$this->where = $e->to_s();
+					$this->where_values = array_flatten($e->values());
+					return;
+				}
+			}
+
+			// no nested array so nothing special to do
+			$this->where = $args[0];
+			$this->where_values = &$values;
+		}
+	}
+
+	private function build_delete()
+	{
+		$sql = "DELETE FROM $this->table";
+
+		if ($this->where)
+			$sql .= " WHERE $this->where";
+
+		if ($this->connection->accepts_limit_and_order_for_update_and_delete())
+		{
+			if ($this->order)
+				$sql .= " ORDER BY $this->order";
+
+			if ($this->limit)
+				$sql = $this->connection->limit($sql,null,$this->limit);
+		}
+
+		return $sql;
+	}
+
+	private function build_insert()
+	{
+		require_once 'Expressions.php';
+		$keys = join(',',$this->quoted_key_names());
+
+		if ($this->sequence)
+		{
+			$sql =
+				"INSERT INTO $this->table($keys," . $this->connection->quote_name($this->sequence[0]) .
+				") VALUES(?," . $this->connection->next_sequence_value($this->sequence[1]) . ")";
+		}
+		else
+			$sql = "INSERT INTO $this->table($keys) VALUES(?)";
+
+		$e = new Expressions($this->connection,$sql,array_values($this->data));
+		return $e->to_s();
+	}
+
+	private function build_select()
+	{
+		$sql = "SELECT $this->select FROM $this->table";
+
+		if ($this->joins)
+			$sql .= ' ' . $this->joins;
+
+		if ($this->where)
+			$sql .= " WHERE $this->where";
+
+		if ($this->group)
+			$sql .= " GROUP BY $this->group";
+
+		if ($this->having)
+			$sql .= " HAVING $this->having";
+
+		if ($this->order)
+			$sql .= " ORDER BY $this->order";
+
+		if ($this->limit || $this->offset)
+			$sql = $this->connection->limit($sql,$this->offset,$this->limit);
+
+		return $sql;
+	}
+
+	private function build_update()
+	{
+		if (strlen($this->update) > 0)
+			$set = $this->update;
+		else
+			$set = join('=?, ', $this->quoted_key_names()) . '=?';
+
+		$sql = "UPDATE $this->table SET $set";
+
+		if ($this->where)
+			$sql .= " WHERE $this->where";
+
+		if ($this->connection->accepts_limit_and_order_for_update_and_delete())
+		{
+			if ($this->order)
+				$sql .= " ORDER BY $this->order";
+
+			if ($this->limit)
+				$sql = $this->connection->limit($sql,null,$this->limit);
+		}
+
+		return $sql;
+	}
+
+	private function quoted_key_names()
+	{
+		$keys = array();
+
+		foreach ($this->data as $key => $value)
+			$keys[] = $this->connection->quote_name($key);
+
+		return $keys;
+	}
+}
+?>

+ 372 - 0
hhvm/php-activerecord/lib/Serialization.php

@@ -0,0 +1,372 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+use XmlWriter;
+
+/**
+ * Base class for Model serializers.
+ *
+ * All serializers support the following options:
+ *
+ * <ul>
+ * <li><b>only:</b> a string or array of attributes to be included.</li>
+ * <li><b>except:</b> a string or array of attributes to be excluded.</li>
+ * <li><b>methods:</b> a string or array of methods to invoke. The method's name will be used as a key for the final attributes array
+ * along with the method's returned value</li>
+ * <li><b>include:</b> a string or array of associated models to include in the final serialized product.</li>
+ * <li><b>only_method:</b> a method that's called and only the resulting array is serialized
+ * <li><b>skip_instruct:</b> set to true to skip the <?xml ...?> declaration.</li>
+ * </ul>
+ *
+ * Example usage:
+ *
+ * <code>
+ * # include the attributes id and name
+ * # run $model->encoded_description() and include its return value
+ * # include the comments association
+ * # include posts association with its own options (nested)
+ * $model->to_json(array(
+ *   'only' => array('id','name', 'encoded_description'),
+ *   'methods' => array('encoded_description'),
+ *   'include' => array('comments', 'posts' => array('only' => 'id'))
+ * ));
+ *
+ * # except the password field from being included
+ * $model->to_xml(array('except' => 'password')));
+ * </code>
+ *
+ * @package ActiveRecord
+ * @link http://www.phpactiverecord.org/guides/utilities#topic-serialization
+ */
+abstract class Serialization
+{
+	protected $model;
+	protected $options;
+	protected $attributes;
+
+	/**
+	 * The default format to serialize DateTime objects to.
+	 *
+	 * @see DateTime
+	 */
+	public static $DATETIME_FORMAT = 'iso8601';
+
+	/**
+	 * Set this to true if the serializer needs to create a nested array keyed
+	 * on the name of the included classes such as for xml serialization.
+	 *
+	 * Setting this to true will produce the following attributes array when
+	 * the include option was used:
+	 *
+	 * <code>
+	 * $user = array('id' => 1, 'name' => 'Tito',
+	 *   'permissions' => array(
+	 *     'permission' => array(
+	 *       array('id' => 100, 'name' => 'admin'),
+	 *       array('id' => 101, 'name' => 'normal')
+	 *     )
+	 *   )
+	 * );
+	 * </code>
+	 *
+	 * Setting to false will produce this:
+	 *
+	 * <code>
+	 * $user = array('id' => 1, 'name' => 'Tito',
+	 *   'permissions' => array(
+	 *     array('id' => 100, 'name' => 'admin'),
+	 *     array('id' => 101, 'name' => 'normal')
+	 *   )
+	 * );
+	 * </code>
+	 *
+	 * @var boolean
+	 */
+	protected $includes_with_class_name_element = false;
+
+	/**
+	 * Constructs a {@link Serialization} object.
+	 *
+	 * @param Model $model The model to serialize
+	 * @param array &$options Options for serialization
+	 * @return Serialization
+	 */
+	public function __construct(Model $model, &$options)
+	{
+		$this->model = $model;
+		$this->options = $options;
+		$this->attributes = $model->attributes();
+		$this->parse_options();
+	}
+
+	private function parse_options()
+	{
+		$this->check_only();
+		$this->check_except();
+		$this->check_methods();
+		$this->check_include();
+		$this->check_only_method();        
+	}
+
+	private function check_only()
+	{
+		if (isset($this->options['only']))
+		{
+			$this->options_to_a('only');
+
+			$exclude = array_diff(array_keys($this->attributes),$this->options['only']);
+			$this->attributes = array_diff_key($this->attributes,array_flip($exclude));
+		}
+	}
+
+	private function check_except()
+	{
+		if (isset($this->options['except']) && !isset($this->options['only']))
+		{
+			$this->options_to_a('except');
+			$this->attributes = array_diff_key($this->attributes,array_flip($this->options['except']));
+		}
+	}
+
+	private function check_methods()
+	{
+		if (isset($this->options['methods']))
+		{
+			$this->options_to_a('methods');
+
+			foreach ($this->options['methods'] as $method)
+			{
+				if (method_exists($this->model, $method))
+					$this->attributes[$method] = $this->model->$method();
+			}
+		}
+	}
+	
+	private function check_only_method()
+	{
+		if (isset($this->options['only_method']))
+		{
+			$method = $this->options['only_method'];
+			if (method_exists($this->model, $method))
+				$this->attributes = $this->model->$method();
+		}
+	}
+
+	private function check_include()
+	{
+		if (isset($this->options['include']))
+		{
+			$this->options_to_a('include');
+
+			$serializer_class = get_class($this);
+
+			foreach ($this->options['include'] as $association => $options)
+			{
+				if (!is_array($options))
+				{
+					$association = $options;
+					$options = array();
+				}
+
+				try {
+					$assoc = $this->model->$association;
+
+					if (!is_array($assoc))
+					{
+						$serialized = new $serializer_class($assoc, $options);
+						$this->attributes[$association] = $serialized->to_a();;
+					}
+					else
+					{
+						$includes = array();
+
+						foreach ($assoc as $a)
+						{
+							$serialized = new $serializer_class($a, $options);
+
+							if ($this->includes_with_class_name_element)
+								$includes[strtolower(get_class($a))][] = $serialized->to_a();
+							else
+								$includes[] = $serialized->to_a();
+						}
+
+						$this->attributes[$association] = $includes;
+					}
+
+				} catch (UndefinedPropertyException $e) {
+					;//move along
+				}
+			}
+		}
+	}
+
+	final protected function options_to_a($key)
+	{
+		if (!is_array($this->options[$key]))
+			$this->options[$key] = array($this->options[$key]);
+	}
+
+	/**
+	 * Returns the attributes array.
+	 * @return array
+	 */
+	final public function to_a()
+	{
+		foreach ($this->attributes as &$value)
+		{
+			if ($value instanceof \DateTime)
+				$value = $value->format(self::$DATETIME_FORMAT);
+		}
+		return $this->attributes;
+	}
+
+	/**
+	 * Returns the serialized object as a string.
+	 * @see to_s
+	 * @return string
+	 */
+	final public function __toString()
+	{
+		return $this->to_s();
+	}
+
+	/**
+	 * Performs the serialization.
+	 * @return string
+	 */
+	abstract public function to_s();
+};
+
+/**
+ * Array serializer.
+ *
+ * @package ActiveRecord
+ */
+class ArraySerializer extends Serialization
+{
+	public static $include_root = false;
+
+	public function to_s()
+	{
+		return self::$include_root ? array(strtolower(get_class($this->model)) => $this->to_a()) : $this->to_a();
+	}
+}
+
+/**
+ * JSON serializer.
+ *
+ * @package ActiveRecord
+ */
+class JsonSerializer extends ArraySerializer
+{
+	public static $include_root = false;
+
+	public function to_s()
+	{
+		parent::$include_root = self::$include_root;
+		return json_encode(parent::to_s());
+	}
+}
+
+/**
+ * XML serializer.
+ *
+ * @package ActiveRecord
+ */
+class XmlSerializer extends Serialization
+{
+	private $writer;
+
+	public function __construct(Model $model, &$options)
+	{
+		$this->includes_with_class_name_element = true;
+		parent::__construct($model,$options);
+	}
+
+	public function to_s()
+	{
+		return $this->xml_encode();
+	}
+
+	private function xml_encode()
+	{
+		$this->writer = new XmlWriter();
+		$this->writer->openMemory();
+		$this->writer->startDocument('1.0', 'UTF-8');
+		$this->writer->startElement(strtolower(denamespace(($this->model))));
+		$this->write($this->to_a());
+		$this->writer->endElement();
+		$this->writer->endDocument();
+		$xml = $this->writer->outputMemory(true);
+
+		if (@$this->options['skip_instruct'] == true)
+			$xml = preg_replace('/<\?xml version.*?\?>/','',$xml);
+
+		return $xml;
+	}
+
+	private function write($data, $tag=null)
+	{
+		foreach ($data as $attr => $value)
+		{
+			if ($tag != null)
+				$attr = $tag;
+
+			if (is_array($value) || is_object($value))
+			{
+				if (!is_int(key($value)))
+				{
+					$this->writer->startElement($attr);
+					$this->write($value);
+					$this->writer->endElement();
+				}
+				else
+					$this->write($value, $attr);
+
+				continue;
+			}
+
+			$this->writer->writeElement($attr, $value);
+		}
+	}
+}
+
+/**
+ * CSV serializer.
+ *
+ * @package ActiveRecord
+ */
+class CsvSerializer extends Serialization
+{
+  public static $delimiter = ',';
+  public static $enclosure = '"';
+
+  public function to_s()
+  {
+    if (@$this->options['only_header'] == true) return $this->header();
+    return $this->row();
+  }
+
+  private function header()
+  {
+    return $this->to_csv(array_keys($this->to_a()));
+  }
+
+  private function row()
+  {
+    return $this->to_csv($this->to_a());
+  }
+
+  private function to_csv($arr)
+  {
+    $outstream = fopen('php://temp', 'w');
+    fputcsv($outstream, $arr, self::$delimiter, self::$enclosure);
+    rewind($outstream);
+    $buffer = trim(stream_get_contents($outstream));
+    fclose($outstream);
+    return $buffer;
+  }
+}
+?>

+ 57 - 0
hhvm/php-activerecord/lib/Singleton.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * This implementation of the singleton pattern does not conform to the strong definition
+ * given by the "Gang of Four." The __construct() method has not be privatized so that
+ * a singleton pattern is capable of being achieved; however, multiple instantiations are also
+ * possible. This allows the user more freedom with this pattern.
+ *
+ * @package ActiveRecord
+ */
+abstract class Singleton
+{
+	/**
+	 * Array of cached singleton objects.
+	 *
+	 * @var array
+	 */
+	private static $instances = array();
+
+	/**
+	 * Static method for instantiating a singleton object.
+	 *
+	 * @return object
+	 */
+	final public static function instance()
+	{
+		$class_name = get_called_class();
+
+		if (!isset(self::$instances[$class_name]))
+			self::$instances[$class_name] = new $class_name;
+
+		return self::$instances[$class_name];
+	}
+
+	/**
+	 * Singleton objects should not be cloned.
+	 *
+	 * @return void
+	 */
+	final private function __clone() {}
+
+	/**
+	 * Similar to a get_called_class() for a child class to invoke.
+	 *
+	 * @return string
+	 */
+	final protected function get_called_class()
+	{
+		$backtrace = debug_backtrace();
+    	return get_class($backtrace[2]['object']);
+	}
+}
+?>

+ 555 - 0
hhvm/php-activerecord/lib/Table.php

@@ -0,0 +1,555 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Manages reading and writing to a database table.
+ *
+ * This class manages a database table and is used by the Model class for
+ * reading and writing to its database table. There is one instance of Table
+ * for every table you have a model for.
+ *
+ * @package ActiveRecord
+ */
+class Table
+{
+	private static $cache = array();
+
+	public $class;
+	public $conn;
+	public $pk;
+	public $last_sql;
+
+	// Name/value pairs of columns in this table
+	public $columns = array();
+
+	/**
+	 * Name of the table.
+	 */
+	public $table;
+
+	/**
+	 * Name of the database (optional)
+	 */
+	public $db_name;
+
+	/**
+	 * Name of the sequence for this table (optional). Defaults to {$table}_seq
+	 */
+	public $sequence;
+
+	/**
+	 * A instance of CallBack for this model/table
+	 * @static
+	 * @var object ActiveRecord\CallBack
+	 */
+	public $callback;
+
+	/**
+	 * List of relationships for this table.
+	 */
+	private $relationships = array();
+
+	public static function load($model_class_name)
+	{
+		if (!isset(self::$cache[$model_class_name]))
+		{
+			/* do not place set_assoc in constructor..it will lead to infinite loop due to
+			   relationships requesting the model's table, but the cache hasn't been set yet */
+			self::$cache[$model_class_name] = new Table($model_class_name);
+			self::$cache[$model_class_name]->set_associations();
+		}
+
+		return self::$cache[$model_class_name];
+	}
+
+	public static function clear_cache($model_class_name=null)
+	{
+		if ($model_class_name && array_key_exists($model_class_name,self::$cache))
+			unset(self::$cache[$model_class_name]);
+		else
+			self::$cache = array();
+	}
+
+	public function __construct($class_name)
+	{
+		$this->class = Reflections::instance()->add($class_name)->get($class_name);
+
+		$this->reestablish_connection(false);
+		$this->set_table_name();
+		$this->get_meta_data();
+		$this->set_primary_key();
+		$this->set_sequence_name();
+		$this->set_delegates();
+		$this->set_setters_and_getters();
+
+		$this->callback = new CallBack($class_name);
+		$this->callback->register('before_save', function(Model $model) { $model->set_timestamps(); }, array('prepend' => true));
+		$this->callback->register('after_save', function(Model $model) { $model->reset_dirty(); }, array('prepend' => true));
+	}
+
+	public function reestablish_connection($close=true)
+	{
+		// if connection name property is null the connection manager will use the default connection
+		$connection = $this->class->getStaticPropertyValue('connection',null);
+
+		if ($close)
+		{
+			ConnectionManager::drop_connection($connection);
+			static::clear_cache();
+		}
+		return ($this->conn = ConnectionManager::get_connection($connection));
+	}
+
+	public function create_joins($joins)
+	{
+		if (!is_array($joins))
+			return $joins;
+
+		$self = $this->table;
+		$ret = $space = '';
+
+		$existing_tables = array();
+		foreach ($joins as $value)
+		{
+			$ret .= $space;
+
+			if (stripos($value,'JOIN ') === false)
+			{
+				if (array_key_exists($value, $this->relationships))
+				{
+					$rel = $this->get_relationship($value);
+
+					// if there is more than 1 join for a given table we need to alias the table names
+					if (array_key_exists($rel->class_name, $existing_tables))
+					{
+						$alias = $value;
+						$existing_tables[$rel->class_name]++;
+					}
+					else
+					{
+						$existing_tables[$rel->class_name] = true;
+						$alias = null;
+					}
+
+					$ret .= $rel->construct_inner_join_sql($this, false, $alias);
+				}
+				else
+					throw new RelationshipException("Relationship named $value has not been declared for class: {$this->class->getName()}");
+			}
+			else
+				$ret .= $value;
+
+			$space = ' ';
+		}
+		return $ret;
+	}
+
+	public function options_to_sql($options)
+	{
+		$table = array_key_exists('from', $options) ? $options['from'] : $this->get_fully_qualified_table_name();
+		$sql = new SQLBuilder($this->conn, $table);
+
+		if (array_key_exists('joins',$options))
+		{
+			$sql->joins($this->create_joins($options['joins']));
+
+			// by default, an inner join will not fetch the fields from the joined table
+			if (!array_key_exists('select', $options))
+				$options['select'] = $this->get_fully_qualified_table_name() . '.*';
+		}
+
+		if (array_key_exists('select',$options))
+			$sql->select($options['select']);
+
+		if (array_key_exists('conditions',$options))
+		{
+			if (!is_hash($options['conditions']))
+			{
+				if (is_string($options['conditions']))
+					$options['conditions'] = array($options['conditions']);
+
+				call_user_func_array(array($sql,'where'),$options['conditions']);
+			}
+			else
+			{
+				if (!empty($options['mapped_names']))
+					$options['conditions'] = $this->map_names($options['conditions'],$options['mapped_names']);
+
+				$sql->where($options['conditions']);
+			}
+		}
+
+		if (array_key_exists('order',$options))
+			$sql->order($options['order']);
+
+		if (array_key_exists('limit',$options))
+			$sql->limit($options['limit']);
+
+		if (array_key_exists('offset',$options))
+			$sql->offset($options['offset']);
+
+		if (array_key_exists('group',$options))
+			$sql->group($options['group']);
+
+		if (array_key_exists('having',$options))
+			$sql->having($options['having']);
+
+		return $sql;
+	}
+
+	public function find($options)
+	{
+		$sql = $this->options_to_sql($options);
+		$readonly = (array_key_exists('readonly',$options) && $options['readonly']) ? true : false;
+		$eager_load = array_key_exists('include',$options) ? $options['include'] : null;
+
+		return $this->find_by_sql($sql->to_s(),$sql->get_where_values(), $readonly, $eager_load);
+	}
+
+	public function find_by_sql($sql, $values=null, $readonly=false, $includes=null)
+	{
+		$this->last_sql = $sql;
+
+		$collect_attrs_for_includes = is_null($includes) ? false : true;
+		$list = $attrs = array();
+		$sth = $this->conn->query($sql,$this->process_data($values));
+
+		while (($row = $sth->fetch()))
+		{
+			$model = new $this->class->name($row,false,true,false);
+
+			if ($readonly)
+				$model->readonly();
+
+			if ($collect_attrs_for_includes)
+				$attrs[] = $model->attributes();
+
+			$list[] = $model;
+		}
+
+		if ($collect_attrs_for_includes && !empty($list))
+			$this->execute_eager_load($list, $attrs, $includes);
+
+		return $list;
+	}
+
+	/**
+	 * Executes an eager load of a given named relationship for this table.
+	 *
+	 * @param $models array found modesl for this table
+	 * @param $attrs array of attrs from $models
+	 * @param $includes array eager load directives
+	 * @return void
+	 */
+	private function execute_eager_load($models=array(), $attrs=array(), $includes=array())
+	{
+		if (!is_array($includes))
+			$includes = array($includes);
+
+		foreach ($includes as $index => $name)
+		{
+			// nested include
+			if (is_array($name))
+			{
+				$nested_includes = count($name) > 1 ? $name : $name[0];
+				$name = $index;
+			}
+			else
+				$nested_includes = array();
+
+			$rel = $this->get_relationship($name, true);
+			$rel->load_eagerly($models, $attrs, $nested_includes, $this);
+		}
+	}
+
+	public function get_column_by_inflected_name($inflected_name)
+	{
+		foreach ($this->columns as $raw_name => $column)
+		{
+			if ($column->inflected_name == $inflected_name)
+				return $column;
+		}
+		return null;
+	}
+
+	public function get_fully_qualified_table_name($quote_name=true)
+	{
+		$table = $quote_name ? $this->conn->quote_name($this->table) : $this->table;
+
+		if ($this->db_name)
+			$table = $this->conn->quote_name($this->db_name) . ".$table";
+
+		return $table;
+	}
+
+	/**
+	 * Retrieve a relationship object for this table. Strict as true will throw an error
+	 * if the relationship name does not exist.
+	 *
+	 * @param $name string name of Relationship
+	 * @param $strict bool
+	 * @throws RelationshipException
+	 * @return Relationship or null
+	 */
+	public function get_relationship($name, $strict=false)
+	{
+		if ($this->has_relationship($name))
+			return $this->relationships[$name];
+
+		if ($strict)
+			throw new RelationshipException("Relationship named $name has not been declared for class: {$this->class->getName()}");
+
+		return null;
+	}
+
+	/**
+	 * Does a given relationship exist?
+	 *
+	 * @param $name string name of Relationship
+	 * @return bool
+	 */
+	public function has_relationship($name)
+	{
+		return array_key_exists($name, $this->relationships);
+	}
+
+	public function insert(&$data, $pk=null, $sequence_name=null)
+	{
+		$data = $this->process_data($data);
+
+		$sql = new SQLBuilder($this->conn,$this->get_fully_qualified_table_name());
+		$sql->insert($data,$pk,$sequence_name);
+
+		$values = array_values($data);
+		return $this->conn->query(($this->last_sql = $sql->to_s()),$values);
+	}
+
+	public function update(&$data, $where)
+	{
+		$data = $this->process_data($data);
+
+		$sql = new SQLBuilder($this->conn,$this->get_fully_qualified_table_name());
+		$sql->update($data)->where($where);
+
+		$values = $sql->bind_values();
+		return $this->conn->query(($this->last_sql = $sql->to_s()),$values);
+	}
+
+	public function delete($data)
+	{
+		$data = $this->process_data($data);
+
+		$sql = new SQLBuilder($this->conn,$this->get_fully_qualified_table_name());
+		$sql->delete($data);
+
+		$values = $sql->bind_values();
+		return $this->conn->query(($this->last_sql = $sql->to_s()),$values);
+	}
+
+	/**
+	 * Add a relationship.
+	 *
+	 * @param Relationship $relationship a Relationship object
+	 */
+	private function add_relationship($relationship)
+	{
+		$this->relationships[$relationship->attribute_name] = $relationship;
+	}
+
+	private function get_meta_data()
+	{
+		// as more adapters are added probably want to do this a better way
+		// than using instanceof but gud enuff for now
+		$quote_name = !($this->conn instanceof PgsqlAdapter);
+
+		$table_name = $this->get_fully_qualified_table_name($quote_name);
+		$conn = $this->conn;
+		$this->columns = Cache::get("get_meta_data-$table_name", function() use ($conn, $table_name) { return $conn->columns($table_name); });
+	}
+
+	/**
+	 * Replaces any aliases used in a hash based condition.
+	 *
+	 * @param $hash array A hash
+	 * @param $map array Hash of used_name => real_name
+	 * @return array Array with any aliases replaced with their read field name
+	 */
+	private function map_names(&$hash, &$map)
+	{
+		$ret = array();
+
+		foreach ($hash as $name => &$value)
+		{
+			if (array_key_exists($name,$map))
+				$name = $map[$name];
+
+			$ret[$name] = $value;
+		}
+		return $ret;
+	}
+
+	private function &process_data($hash)
+	{
+		if (!$hash)
+			return $hash;
+
+		foreach ($hash as $name => &$value)
+		{
+			if ($value instanceof \DateTime)
+			{
+				if (isset($this->columns[$name]) && $this->columns[$name]->type == Column::DATE)
+					$hash[$name] = $this->conn->date_to_string($value);
+				else
+					$hash[$name] = $this->conn->datetime_to_string($value);
+			}
+			else
+				$hash[$name] = $value;
+		}
+		return $hash;
+	}
+
+	private function set_primary_key()
+	{
+		if (($pk = $this->class->getStaticPropertyValue('pk',null)) || ($pk = $this->class->getStaticPropertyValue('primary_key',null)))
+			$this->pk = is_array($pk) ? $pk : array($pk);
+		else
+		{
+			$this->pk = array();
+
+			foreach ($this->columns as $c)
+			{
+				if ($c->pk)
+					$this->pk[] = $c->inflected_name;
+			}
+		}
+	}
+
+	private function set_table_name()
+	{
+		if (($table = $this->class->getStaticPropertyValue('table',null)) || ($table = $this->class->getStaticPropertyValue('table_name',null)))
+			$this->table = $table;
+		else
+		{
+			// infer table name from the class name
+			$this->table = Inflector::instance()->tableize($this->class->getName());
+
+			// strip namespaces from the table name if any
+			$parts = explode('\\',$this->table);
+			$this->table = $parts[count($parts)-1];
+		}
+
+		if(($db = $this->class->getStaticPropertyValue('db',null)) || ($db = $this->class->getStaticPropertyValue('db_name',null)))
+			$this->db_name = $db;
+	}
+
+	private function set_sequence_name()
+	{
+		if (!$this->conn->supports_sequences())
+			return;
+
+		if (!($this->sequence = $this->class->getStaticPropertyValue('sequence')))
+			$this->sequence = $this->conn->get_sequence_name($this->table,$this->pk[0]);
+	}
+
+	private function set_associations()
+	{
+		require_once 'Relationship.php';
+		$namespace = $this->class->getNamespaceName();
+
+		foreach ($this->class->getStaticProperties() as $name => $definitions)
+		{
+			if (!$definitions)# || !is_array($definitions))
+				continue;
+
+			foreach (wrap_strings_in_arrays($definitions) as $definition)
+			{
+				$relationship = null;
+				$definition += compact('namespace');
+
+				switch ($name)
+				{
+					case 'has_many':
+						$relationship = new HasMany($definition);
+						break;
+
+					case 'has_one':
+						$relationship = new HasOne($definition);
+						break;
+
+					case 'belongs_to':
+						$relationship = new BelongsTo($definition);
+						break;
+
+					case 'has_and_belongs_to_many':
+						$relationship = new HasAndBelongsToMany($definition);
+						break;
+				}
+
+				if ($relationship)
+					$this->add_relationship($relationship);
+			}
+		}
+	}
+
+	/**
+	 * Rebuild the delegates array into format that we can more easily work with in Model.
+	 * Will end up consisting of array of:
+	 *
+	 * array('delegate' => array('field1','field2',...),
+	 *       'to'       => 'delegate_to_relationship',
+	 *       'prefix'	=> 'prefix')
+	 */
+	private function set_delegates()
+	{
+		$delegates = $this->class->getStaticPropertyValue('delegate',array());
+		$new = array();
+
+		if (!array_key_exists('processed', $delegates))
+			$delegates['processed'] = false;
+
+		if (!empty($delegates) && !$delegates['processed'])
+		{
+			foreach ($delegates as &$delegate)
+			{
+				if (!is_array($delegate) || !isset($delegate['to']))
+					continue;
+
+				if (!isset($delegate['prefix']))
+					$delegate['prefix'] = null;
+
+				$new_delegate = array(
+					'to'		=> $delegate['to'],
+					'prefix'	=> $delegate['prefix'],
+					'delegate'	=> array());
+
+				foreach ($delegate as $name => $value)
+				{
+					if (is_numeric($name))
+						$new_delegate['delegate'][] = $value;
+				}
+
+				$new[] = $new_delegate;
+			}
+
+			$new['processed'] = true;
+			$this->class->setStaticPropertyValue('delegate',$new);
+		}
+	}
+
+	/**
+	 * @deprecated Model.php now checks for get|set_ methods via method_exists so there is no need for declaring static g|setters.
+	 */
+	private function set_setters_and_getters()
+	{
+		$getters = $this->class->getStaticPropertyValue('getters', array());
+		$setters = $this->class->getStaticPropertyValue('setters', array());
+
+		if (!empty($getters) || !empty($setters))
+			trigger_error('static::$getters and static::$setters are deprecated. Please define your setters and getters by declaring methods in your model prefixed with get_ or set_. See
+			http://www.phpactiverecord.org/projects/main/wiki/Utilities#attribute-setters and http://www.phpactiverecord.org/projects/main/wiki/Utilities#attribute-getters on how to make use of this option.', E_USER_DEPRECATED);
+	}
+};
+?>

+ 359 - 0
hhvm/php-activerecord/lib/Utils.php

@@ -0,0 +1,359 @@
+<?php
+/**
+ *
+ * @package ActiveRecord
+ */
+
+/*
+ * Thanks to http://www.eval.ca/articles/php-pluralize (MIT license)
+ *           http://dev.rubyonrails.org/browser/trunk/activesupport/lib/active_support/inflections.rb (MIT license)
+ *           http://www.fortunecity.com/bally/durrus/153/gramch13.html
+ *           http://www2.gsu.edu/~wwwesl/egw/crump.htm
+ *
+ * Changes (12/17/07)
+ *   Major changes
+ *   --
+ *   Fixed irregular noun algorithm to use regular expressions just like the original Ruby source.
+ *       (this allows for things like fireman -> firemen
+ *   Fixed the order of the singular array, which was backwards.
+ *
+ *   Minor changes
+ *   --
+ *   Removed incorrect pluralization rule for /([^aeiouy]|qu)ies$/ => $1y
+ *   Expanded on the list of exceptions for *o -> *oes, and removed rule for buffalo -> buffaloes
+ *   Removed dangerous singularization rule for /([^f])ves$/ => $1fe
+ *   Added more specific rules for singularizing lives, wives, knives, sheaves, loaves, and leaves and thieves
+ *   Added exception to /(us)es$/ => $1 rule for houses => house and blouses => blouse
+ *   Added excpetions for feet, geese and teeth
+ *   Added rule for deer -> deer
+ *
+ * Changes:
+ *   Removed rule for virus -> viri
+ *   Added rule for potato -> potatoes
+ *   Added rule for *us -> *uses
+ */
+namespace ActiveRecord;
+
+use \Closure;
+
+function classify($class_name, $singularize=false)
+{
+	if ($singularize)
+    $class_name = Utils::singularize($class_name);
+
+	$class_name = Inflector::instance()->camelize($class_name);
+	return ucfirst($class_name);
+}
+
+// http://snippets.dzone.com/posts/show/4660
+function array_flatten(array $array)
+{
+	$i = 0;
+
+	while ($i < count($array))
+	{
+		if (is_array($array[$i]))
+			array_splice($array,$i,1,$array[$i]);
+		else
+			++$i;
+	}
+	return $array;
+}
+
+/**
+ * Somewhat naive way to determine if an array is a hash.
+ */
+function is_hash(&$array)
+{
+	if (!is_array($array))
+		return false;
+
+	$keys = array_keys($array);
+	return @is_string($keys[0]) ? true : false;
+}
+
+/**
+ * Strips a class name of any namespaces and namespace operator.
+ *
+ * @param string $class
+ * @return string stripped class name
+ * @access public
+ */
+function denamespace($class_name)
+{
+	if (is_object($class_name))
+		$class_name = get_class($class_name);
+
+	if (has_namespace($class_name))
+	{
+		$parts = explode('\\', $class_name);
+		return end($parts);
+	}
+	return $class_name;
+}
+
+function get_namespaces($class_name)
+{
+	if (has_namespace($class_name))
+		return explode('\\', $class_name);
+	return null;
+}
+
+function has_namespace($class_name)
+{
+	if (strpos($class_name, '\\') !== false)
+		return true;
+	return false;
+}
+
+/**
+ * Returns true if all values in $haystack === $needle
+ * @param $needle
+ * @param $haystack
+ * @return unknown_type
+ */
+function all($needle, array $haystack)
+{
+	foreach ($haystack as $value)
+	{
+		if ($value !== $needle)
+			return false;
+	}
+	return true;
+}
+
+function collect(&$enumerable, $name_or_closure)
+{
+	$ret = array();
+
+	foreach ($enumerable as $value)
+	{
+		if (is_string($name_or_closure))
+			$ret[] = is_array($value) ? $value[$name_or_closure] : $value->$name_or_closure;
+		elseif ($name_or_closure instanceof Closure)
+			$ret[] = $name_or_closure($value);
+	}
+	return $ret;
+}
+
+/**
+ * Wrap string definitions (if any) into arrays.
+ */
+function wrap_strings_in_arrays(&$strings)
+{
+	if (!is_array($strings))
+		$strings = array(array($strings));
+	else 
+	{
+		foreach ($strings as &$str)
+		{
+			if (!is_array($str))
+				$str = array($str);
+		}
+	}
+	return $strings;
+}
+
+/**
+ * Some internal utility functions.
+ *
+ * @package ActiveRecord
+ */
+class Utils
+{
+	public static function extract_options($options)
+	{
+		return is_array(end($options)) ? end($options) : array();
+	}
+
+	public static function add_condition(&$conditions=array(), $condition, $conjuction='AND')
+	{
+		if (is_array($condition))
+		{
+			if (empty($conditions))
+				$conditions = array_flatten($condition);
+			else
+			{
+				$conditions[0] .= " $conjuction " . array_shift($condition);
+				$conditions[] = array_flatten($condition);
+			}
+		}
+		elseif (is_string($condition))
+			$conditions[0] .= " $conjuction $condition";
+
+		return $conditions;
+	}
+
+	public static function human_attribute($attr)
+	{
+		$inflector = Inflector::instance();
+		$inflected = $inflector->variablize($attr);
+		$normal = $inflector->uncamelize($inflected);
+
+		return ucfirst(str_replace('_', ' ', $normal));
+	}
+
+	public static function is_odd($number)
+	{
+		return $number & 1;
+	}
+
+	public static function is_a($type, $var)
+	{
+		switch($type)
+		{
+			case 'range':
+				if (is_array($var) && (int)$var[0] < (int)$var[1])
+					return true;
+
+		}
+
+		return false;
+	}
+
+	public static function is_blank($var)
+	{
+		return 0 === strlen($var);
+	}
+
+	private static $plural = array(
+        '/(quiz)$/i'               => "$1zes",
+        '/^(ox)$/i'                => "$1en",
+        '/([m|l])ouse$/i'          => "$1ice",
+        '/(matr|vert|ind)ix|ex$/i' => "$1ices",
+        '/(x|ch|ss|sh)$/i'         => "$1es",
+        '/([^aeiouy]|qu)y$/i'      => "$1ies",
+        '/(hive)$/i'               => "$1s",
+        '/(?:([^f])fe|([lr])f)$/i' => "$1$2ves",
+        '/(shea|lea|loa|thie)f$/i' => "$1ves",
+        '/sis$/i'                  => "ses",
+        '/([ti])um$/i'             => "$1a",
+        '/(tomat|potat|ech|her|vet)o$/i'=> "$1oes",
+        '/(bu)s$/i'                => "$1ses",
+        '/(alias)$/i'              => "$1es",
+        '/(octop)us$/i'            => "$1i",
+        '/(ax|test)is$/i'          => "$1es",
+        '/(us)$/i'                 => "$1es",
+        '/s$/i'                    => "s",
+        '/$/'                      => "s"
+    );
+
+    private static $singular = array(
+        '/(quiz)zes$/i'             => "$1",
+        '/(matr)ices$/i'            => "$1ix",
+        '/(vert|ind)ices$/i'        => "$1ex",
+        '/^(ox)en$/i'               => "$1",
+        '/(alias)es$/i'             => "$1",
+        '/(octop|vir)i$/i'          => "$1us",
+        '/(cris|ax|test)es$/i'      => "$1is",
+        '/(shoe)s$/i'               => "$1",
+        '/(o)es$/i'                 => "$1",
+        '/(bus)es$/i'               => "$1",
+        '/([m|l])ice$/i'            => "$1ouse",
+        '/(x|ch|ss|sh)es$/i'        => "$1",
+        '/(m)ovies$/i'              => "$1ovie",
+        '/(s)eries$/i'              => "$1eries",
+        '/([^aeiouy]|qu)ies$/i'     => "$1y",
+        '/([lr])ves$/i'             => "$1f",
+        '/(tive)s$/i'               => "$1",
+        '/(hive)s$/i'               => "$1",
+        '/(li|wi|kni)ves$/i'        => "$1fe",
+        '/(shea|loa|lea|thie)ves$/i'=> "$1f",
+        '/(^analy)ses$/i'           => "$1sis",
+        '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i'  => "$1$2sis",
+        '/([ti])a$/i'               => "$1um",
+        '/(n)ews$/i'                => "$1ews",
+        '/(h|bl)ouses$/i'           => "$1ouse",
+        '/(corpse)s$/i'             => "$1",
+        '/(us)es$/i'                => "$1",
+        '/(us|ss)$/i'               => "$1",
+        '/s$/i'                     => ""
+    );
+
+    private static $irregular = array(
+        'move'   => 'moves',
+        'foot'   => 'feet',
+        'goose'  => 'geese',
+        'sex'    => 'sexes',
+        'child'  => 'children',
+        'man'    => 'men',
+        'tooth'  => 'teeth',
+        'person' => 'people'
+    );
+
+    private static $uncountable = array(
+        'sheep',
+        'fish',
+        'deer',
+        'series',
+        'species',
+        'money',
+        'rice',
+        'information',
+        'equipment'
+    );
+
+    public static function pluralize( $string )
+    {
+        // save some time in the case that singular and plural are the same
+        if ( in_array( strtolower( $string ), self::$uncountable ) )
+            return $string;
+
+        // check for irregular singular forms
+        foreach ( self::$irregular as $pattern => $result )
+        {
+            $pattern = '/' . $pattern . '$/i';
+
+            if ( preg_match( $pattern, $string ) )
+                return preg_replace( $pattern, $result, $string);
+        }
+
+        // check for matches using regular expressions
+        foreach ( self::$plural as $pattern => $result )
+        {
+            if ( preg_match( $pattern, $string ) )
+                return preg_replace( $pattern, $result, $string );
+        }
+
+        return $string;
+    }
+
+    public static function singularize( $string )
+    {
+        // save some time in the case that singular and plural are the same
+        if ( in_array( strtolower( $string ), self::$uncountable ) )
+            return $string;
+
+        // check for irregular plural forms
+        foreach ( self::$irregular as $result => $pattern )
+        {
+            $pattern = '/' . $pattern . '$/i';
+
+            if ( preg_match( $pattern, $string ) )
+                return preg_replace( $pattern, $result, $string);
+        }
+
+        // check for matches using regular expressions
+        foreach ( self::$singular as $pattern => $result )
+        {
+            if ( preg_match( $pattern, $string ) )
+                return preg_replace( $pattern, $result, $string );
+        }
+
+        return $string;
+    }
+
+    public static function pluralize_if($count, $string)
+    {
+        if ($count == 1)
+            return $string;
+        else
+            return self::pluralize($string);
+    }
+
+	public static function squeeze($char, $string)
+	{
+		return preg_replace("/$char+/",$char,$string);
+	}
+};
+?>

+ 912 - 0
hhvm/php-activerecord/lib/Validations.php

@@ -0,0 +1,912 @@
+<?php
+/**
+ * These two classes have been <i>heavily borrowed</i> from Ruby on Rails' ActiveRecord so much that
+ * this piece can be considered a straight port. The reason for this is that the vaildation process is
+ * tricky due to order of operations/events. The former combined with PHP's odd typecasting means
+ * that it was easier to formulate this piece base on the rails code.
+ *
+ * @package ActiveRecord
+ */
+
+namespace ActiveRecord;
+use ActiveRecord\Model;
+use IteratorAggregate;
+use ArrayIterator;
+
+/**
+ * Manages validations for a {@link Model}.
+ *
+ * This class isn't meant to be directly used. Instead you define
+ * validators thru static variables in your {@link Model}. Example:
+ *
+ * <code>
+ * class Person extends ActiveRecord\Model {
+ *   static $validates_length_of = array(
+ *     array('name', 'within' => array(30,100),
+ *     array('state', 'is' => 2)
+ *   );
+ * }
+ *
+ * $person = new Person();
+ * $person->name = 'Tito';
+ * $person->state = 'this is not two characters';
+ *
+ * if (!$person->is_valid())
+ *   print_r($person->errors);
+ * </code>
+ *
+ * @package ActiveRecord
+ * @see Errors
+ * @link http://www.phpactiverecord.org/guides/validations
+ */
+class Validations
+{
+	private $model;
+	private $options = array();
+	private $validators = array();
+	private $record;
+
+	private static $VALIDATION_FUNCTIONS = array(
+		'validates_presence_of',
+		'validates_size_of',
+		'validates_length_of',
+		'validates_inclusion_of',
+		'validates_exclusion_of',
+		'validates_format_of',
+		'validates_numericality_of',
+		'validates_uniqueness_of'
+	);
+
+	private static $DEFAULT_VALIDATION_OPTIONS = array(
+		'on' => 'save',
+		'allow_null' => false,
+		'allow_blank' => false,
+		'message' => null,
+	);
+
+	private static  $ALL_RANGE_OPTIONS = array(
+		'is' => null,
+		'within' => null,
+		'in' => null,
+		'minimum' => null,
+		'maximum' => null,
+	);
+
+	private static $ALL_NUMERICALITY_CHECKS = array(
+		'greater_than' => null,
+		'greater_than_or_equal_to'  => null,
+		'equal_to' => null,
+		'less_than' => null,
+		'less_than_or_equal_to' => null,
+		'odd' => null,
+		'even' => null
+	);
+
+	/**
+	 * Constructs a {@link Validations} object.
+	 *
+	 * @param Model $model The model to validate
+	 * @return Validations
+	 */
+	public function __construct(Model $model)
+	{
+		$this->model = $model;
+		$this->record = new Errors($this->model);
+		$this->klass = Reflections::instance()->get(get_class($this->model));
+		$this->validators = array_intersect(array_keys($this->klass->getStaticProperties()), self::$VALIDATION_FUNCTIONS);
+	}
+
+	public function get_record()
+	{
+		return $this->record;
+	}
+
+	/**
+	 * Returns validator data.
+	 *
+	 * @return array
+	 */
+	public function rules()
+	{
+		$data = array();
+		foreach ($this->validators as $validate)
+		{
+			$attrs = $this->klass->getStaticPropertyValue($validate);
+
+			foreach (wrap_strings_in_arrays($attrs) as $attr)
+			{
+				$field = $attr[0];
+
+				if (!isset($data[$field]) || !is_array($data[$field]))
+					$data[$field] = array();
+
+				$attr['validator'] = $validate;
+				unset($attr[0]);
+				array_push($data[$field],$attr);
+			}
+		}
+		return $data;
+	}
+
+	/**
+	 * Runs the validators.
+	 *
+	 * @return Errors the validation errors if any
+	 */
+	public function validate()
+	{
+		foreach ($this->validators as $validate)
+		{
+			$definition = $this->klass->getStaticPropertyValue($validate);
+			$this->$validate(wrap_strings_in_arrays($definition));
+		}
+
+		$model_reflection = Reflections::instance()->get($this->model);
+
+		if ($model_reflection->hasMethod('validate') && $model_reflection->getMethod('validate')->isPublic())
+			$this->model->validate();
+
+		$this->record->clear_model();
+		return $this->record;
+	}
+
+	/**
+	 * Validates a field is not null and not blank.
+	 *
+	 * <code>
+	 * class Person extends ActiveRecord\Model {
+	 *   static $validates_presence_of = array(
+	 *     array('first_name'),
+	 *     array('last_name')
+	 *   );
+	 * }
+	 * </code>
+	 *
+	 * Available options:
+	 *
+	 * <ul>
+	 * <li><b>message:</b> custom error message</li>
+	 * <li><b>allow_blank:</b> allow blank strings</li>
+	 * <li><b>allow_null:</b> allow null strings</li>
+	 * </ul>
+	 *
+	 * @param array $attrs Validation definition
+	 */
+	public function validates_presence_of($attrs)
+	{
+		$configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array('message' => Errors::$DEFAULT_ERROR_MESSAGES['blank'], 'on' => 'save'));
+
+		foreach ($attrs as $attr)
+		{
+			$options = array_merge($configuration, $attr);
+			$this->record->add_on_blank($options[0], $options['message']);
+		}
+	}
+
+	/**
+	 * Validates that a value is included the specified array.
+	 *
+	 * <code>
+	 * class Car extends ActiveRecord\Model {
+	 *   static $validates_inclusion_of = array(
+	 *     array('fuel_type', 'in' => array('hyrdogen', 'petroleum', 'electric')),
+	 *   );
+	 * }
+	 * </code>
+	 *
+	 * Available options:
+	 *
+	 * <ul>
+	 * <li><b>in/within:</b> attribute should/shouldn't be a value within an array</li>
+	 * <li><b>message:</b> custome error message</li>
+	 * <li><b>allow_blank:</b> allow blank strings</li>
+	 * <li><b>allow_null:</b> allow null strings</li>
+	 * </ul>
+	 *
+	 * @param array $attrs Validation definition
+	 */
+	public function validates_inclusion_of($attrs)
+	{
+		$this->validates_inclusion_or_exclusion_of('inclusion', $attrs);
+	}
+
+	/**
+	 * This is the opposite of {@link validates_include_of}.
+	 *
+	 * Available options:
+	 *
+	 * <ul>
+	 * <li><b>in/within:</b> attribute should/shouldn't be a value within an array</li>
+	 * <li><b>message:</b> custome error message</li>
+	 * <li><b>allow_blank:</b> allow blank strings</li>
+	 * <li><b>allow_null:</b> allow null strings</li>
+	 * </ul>
+	 *
+	 * @param array $attrs Validation definition
+	 * @see validates_inclusion_of
+	 */
+	public function validates_exclusion_of($attrs)
+	{
+		$this->validates_inclusion_or_exclusion_of('exclusion', $attrs);
+	}
+
+	/**
+	 * Validates that a value is in or out of a specified list of values.
+	 *
+	 * Available options:
+	 *
+	 * <ul>
+	 * <li><b>in/within:</b> attribute should/shouldn't be a value within an array</li>
+	 * <li><b>message:</b> custome error message</li>
+	 * <li><b>allow_blank:</b> allow blank strings</li>
+	 * <li><b>allow_null:</b> allow null strings</li>
+	 * </ul>
+	 *
+	 * @see validates_inclusion_of
+	 * @see validates_exclusion_of
+	 * @param string $type Either inclusion or exclusion
+	 * @param $attrs Validation definition
+	 */
+	public function validates_inclusion_or_exclusion_of($type, $attrs)
+	{
+		$configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array('message' => Errors::$DEFAULT_ERROR_MESSAGES[$type], 'on' => 'save'));
+
+		foreach ($attrs as $attr)
+		{
+			$options = array_merge($configuration, $attr);
+			$attribute = $options[0];
+			$var = $this->model->$attribute;
+
+			if (isset($options['in']))
+				$enum = $options['in'];
+			elseif (isset($options['within']))
+				$enum = $options['within'];
+
+			if (!is_array($enum))
+				array($enum);
+
+			$message = str_replace('%s', $var, $options['message']);
+
+			if ($this->is_null_with_option($var, $options) || $this->is_blank_with_option($var, $options))
+				continue;
+
+			if (('inclusion' == $type && !in_array($var, $enum)) || ('exclusion' == $type && in_array($var, $enum)))
+				$this->record->add($attribute, $message);
+		}
+	}
+
+	/**
+	 * Validates that a value is numeric.
+	 *
+	 * <code>
+	 * class Person extends ActiveRecord\Model {
+	 *   static $validates_numericality_of = array(
+	 *     array('salary', 'greater_than' => 19.99, 'less_than' => 99.99)
+	 *   );
+	 * }
+	 * </code>
+	 *
+	 * Available options:
+	 *
+	 * <ul>
+	 * <li><b>only_integer:</b> value must be an integer (e.g. not a float)</li>
+	 * <li><b>even:</b> must be even</li>
+	 * <li><b>odd:</b> must be odd"</li>
+	 * <li><b>greater_than:</b> must be greater than specified number</li>
+	 * <li><b>greater_than_or_equal_to:</b> must be greater than or equal to specified number</li>
+	 * <li><b>equal_to:</b> ...</li>
+	 * <li><b>less_than:</b> ...</li>
+	 * <li><b>less_than_or_equal_to:</b> ...</li>
+	 * <li><b>allow_blank:</b> allow blank strings</li>
+	 * <li><b>allow_null:</b> allow null strings</li>
+	 * </ul>
+	 *
+	 * @param array $attrs Validation definition
+	 */
+	public function validates_numericality_of($attrs)
+	{
+		$configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array('only_integer' => false));
+
+		// Notice that for fixnum and float columns empty strings are converted to nil.
+		// Validates whether the value of the specified attribute is numeric by trying to convert it to a float with Kernel.Float
+		// (if only_integer is false) or applying it to the regular expression /\A[+\-]?\d+\Z/ (if only_integer is set to true).
+		foreach ($attrs as $attr)
+		{
+			$options = array_merge($configuration, $attr);
+			$attribute = $options[0];
+			$var = $this->model->$attribute;
+
+			$numericalityOptions = array_intersect_key(self::$ALL_NUMERICALITY_CHECKS, $options);
+
+			if ($this->is_null_with_option($var, $options))
+				continue;
+
+			$not_a_number_message = (isset($options['message']) ? $options['message'] : Errors::$DEFAULT_ERROR_MESSAGES['not_a_number']);
+
+			if (true === $options['only_integer'] && !is_integer($var))
+			{
+				if (!preg_match('/\A[+-]?\d+\Z/', (string)($var)))
+				{
+					$this->record->add($attribute, $not_a_number_message);
+					continue;
+				}
+			}
+			else
+			{
+				if (!is_numeric($var))
+				{
+					$this->record->add($attribute, $not_a_number_message);
+					continue;
+				}
+
+				$var = (float)$var;
+			}
+
+			foreach ($numericalityOptions as $option => $check)
+			{
+				$option_value = $options[$option];
+				$message = (isset($options['message']) ? $options['message'] : Errors::$DEFAULT_ERROR_MESSAGES[$option]);
+
+				if ('odd' != $option && 'even' != $option)
+				{
+					$option_value = (float)$options[$option];
+
+					if (!is_numeric($option_value))
+						throw new ValidationsArgumentError("$option must be a number");
+
+					$message = str_replace('%d', $option_value, $message);
+
+					if ('greater_than' == $option && !($var > $option_value))
+						$this->record->add($attribute, $message);
+
+					elseif ('greater_than_or_equal_to' == $option && !($var >= $option_value))
+						$this->record->add($attribute, $message);
+
+					elseif ('equal_to' == $option && !($var == $option_value))
+						$this->record->add($attribute, $message);
+
+					elseif ('less_than' == $option && !($var < $option_value))
+						$this->record->add($attribute, $message);
+
+					elseif ('less_than_or_equal_to' == $option && !($var <= $option_value))
+						$this->record->add($attribute, $message);
+				}
+				else
+				{
+					if (('odd' == $option && !Utils::is_odd($var)) || ('even' == $option && Utils::is_odd($var)))
+						$this->record->add($attribute, $message);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Alias of {@link validates_length_of}
+	 *
+	 * @param array $attrs Validation definition
+	 */
+	public function validates_size_of($attrs)
+	{
+		$this->validates_length_of($attrs);
+	}
+
+	/**
+	 * Validates that a value is matches a regex.
+	 *
+	 * <code>
+	 * class Person extends ActiveRecord\Model {
+	 *   static $validates_format_of = array(
+	 *     array('email', 'with' => '/^.*?@.*$/')
+	 *   );
+	 * }
+	 * </code>
+	 *
+	 * Available options:
+	 *
+	 * <ul>
+	 * <li><b>with:</b> a regular expression</li>
+	 * <li><b>message:</b> custom error message</li>
+	 * <li><b>allow_blank:</b> allow blank strings</li>
+	 * <li><b>allow_null:</b> allow null strings</li>
+	 * </ul>
+	 *
+	 * @param array $attrs Validation definition
+	 */
+	public function validates_format_of($attrs)
+	{
+		$configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array('message' => Errors::$DEFAULT_ERROR_MESSAGES['invalid'], 'on' => 'save', 'with' => null));
+
+		foreach ($attrs as $attr)
+		{
+			$options = array_merge($configuration, $attr);
+			$attribute = $options[0];
+			$var = $this->model->$attribute;
+
+			if (is_null($options['with']) || !is_string($options['with']) || !is_string($options['with']))
+				throw new ValidationsArgumentError('A regular expression must be supplied as the [with] option of the configuration array.');
+			else
+				$expression = $options['with'];
+
+			if ($this->is_null_with_option($var, $options) || $this->is_blank_with_option($var, $options))
+				continue;
+
+			if (!@preg_match($expression, $var))
+			$this->record->add($attribute, $options['message']);
+		}
+	}
+
+	/**
+	 * Validates the length of a value.
+	 *
+	 * <code>
+	 * class Person extends ActiveRecord\Model {
+	 *   static $validates_length_of = array(
+	 *     array('name', 'within' => array(1,50))
+	 *   );
+	 * }
+	 * </code>
+	 *
+	 * Available options:
+	 *
+	 * <ul>
+	 * <li><b>is:</b> attribute should be exactly n characters long</li>
+	 * <li><b>in/within:</b> attribute should be within an range array(min,max)</li>
+	 * <li><b>maximum/minimum:</b> attribute should not be above/below respectively</li>
+	 * <li><b>message:</b> custome error message</li>
+	 * <li><b>allow_blank:</b> allow blank strings</li>
+	 * <li><b>allow_null:</b> allow null strings. (Even if this is set to false, a null string is always shorter than a maximum value.)</li>
+	 * </ul>
+	 *
+	 * @param array $attrs Validation definition
+	 */
+	public function validates_length_of($attrs)
+	{
+		$configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array(
+			'too_long'     => Errors::$DEFAULT_ERROR_MESSAGES['too_long'],
+			'too_short'    => Errors::$DEFAULT_ERROR_MESSAGES['too_short'],
+			'wrong_length' => Errors::$DEFAULT_ERROR_MESSAGES['wrong_length']
+		));
+
+		foreach ($attrs as $attr)
+		{
+			$options = array_merge($configuration, $attr);
+			$range_options = array_intersect(array_keys(self::$ALL_RANGE_OPTIONS), array_keys($attr));
+			sort($range_options);
+
+			switch (sizeof($range_options))
+			{
+				case 0:
+					throw new  ValidationsArgumentError('Range unspecified.  Specify the [within], [maximum], or [is] option.');
+
+				case 1:
+					break;
+
+				default:
+					throw new  ValidationsArgumentError('Too many range options specified.  Choose only one.');
+			}
+
+			$attribute = $options[0];
+			$var = $this->model->$attribute;
+			if ($this->is_null_with_option($var, $options) || $this->is_blank_with_option($var, $options))
+				continue;
+			if ($range_options[0] == 'within' || $range_options[0] == 'in')
+			{
+				$range = $options[$range_options[0]];
+
+				if (!(Utils::is_a('range', $range)))
+					throw new  ValidationsArgumentError("$range_option must be an array composing a range of numbers with key [0] being less than key [1]");
+				$range_options = array('minimum', 'maximum');
+				$attr['minimum'] = $range[0];
+				$attr['maximum'] = $range[1];
+			}
+			foreach ($range_options as $range_option)
+			{
+				$option = $attr[$range_option];
+
+				if ((int)$option <= 0)
+					throw new  ValidationsArgumentError("$range_option value cannot use a signed integer.");
+
+				if (is_float($option))
+					throw new  ValidationsArgumentError("$range_option value cannot use a float for length.");
+
+				if (!($range_option == 'maximum' && is_null($this->model->$attribute)))
+				{
+					$messageOptions = array('is' => 'wrong_length', 'minimum' => 'too_short', 'maximum' => 'too_long');
+
+					if (isset($options['message']))
+						$message = $options['message'];
+					else
+						$message = $options[$messageOptions[$range_option]];
+					
+
+					$message = str_replace('%d', $option, $message);
+					$attribute_value = $this->model->$attribute;
+					$len = strlen($attribute_value);
+					$value = (int)$attr[$range_option];
+
+					if ('maximum' == $range_option && $len > $value)
+						$this->record->add($attribute, $message);
+
+					if ('minimum' == $range_option && $len < $value)
+						$this->record->add($attribute, $message);
+
+					if ('is' == $range_option && $len !== $value)
+						$this->record->add($attribute, $message);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Validates the uniqueness of a value.
+	 *
+	 * <code>
+	 * class Person extends ActiveRecord\Model {
+	 *   static $validates_uniqueness_of = array(
+	 *     array('name'),
+	 *     array(array('blah','bleh'), 'message' => 'blech')
+	 *   );
+	 * }
+	 * </code>
+	 *
+	 * Available options:
+	 *
+	 * <ul>
+	 * <li><b>with:</b> a regular expression</li>
+	 * <li><b>message:</b> custom error message</li>
+	 * <li><b>allow_blank:</b> allow blank strings</li>
+	 * <li><b>allow_null:</b> allow null strings</li>
+	 * </ul>
+	 *
+	 * @param array $attrs Validation definition
+	 */
+	public function validates_uniqueness_of($attrs)
+	{
+		$configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array(
+			'message' => Errors::$DEFAULT_ERROR_MESSAGES['unique']
+		));
+		// Retrieve connection from model for quote_name method
+		$connection = $this->klass->getMethod('connection')->invoke(null);
+
+		foreach ($attrs as $attr)
+		{
+			$options = array_merge($configuration, $attr);
+			$pk = $this->model->get_primary_key();
+			$pk_value = $this->model->$pk[0];
+
+			if (is_array($options[0]))
+			{
+				$add_record = join("_and_", $options[0]);
+				$fields = $options[0];
+			}
+			else
+			{
+				$add_record = $options[0];
+				$fields = array($options[0]);
+			}
+
+			$sql = "";
+			$conditions = array("");
+			$pk_quoted = $connection->quote_name($pk[0]);
+			if ($pk_value === null)
+				$sql = "{$pk_quoted} IS NOT NULL";
+			else
+			{
+				$sql = "{$pk_quoted} != ?";
+				array_push($conditions,$pk_value);
+			}
+
+			foreach ($fields as $field)
+			{
+				$field = $this->model->get_real_attribute_name($field);
+				$quoted_field = $connection->quote_name($field);
+				$sql .= " AND {$quoted_field}=?";
+				array_push($conditions,$this->model->$field);
+			}
+
+			$conditions[0] = $sql;
+
+			if ($this->model->exists(array('conditions' => $conditions)))
+				$this->record->add($add_record, $options['message']);
+		}
+	}
+
+	private function is_null_with_option($var, &$options)
+	{
+		return (is_null($var) && (isset($options['allow_null']) && $options['allow_null']));
+	}
+
+	private function is_blank_with_option($var, &$options)
+	{
+		return (Utils::is_blank($var) && (isset($options['allow_blank']) && $options['allow_blank']));
+	}
+}
+
+/**
+ * Class that holds {@link Validations} errors.
+ *
+ * @package ActiveRecord
+ */
+class Errors implements IteratorAggregate
+{
+	private $model;
+	private $errors;
+
+	public static $DEFAULT_ERROR_MESSAGES = array(
+		'inclusion'    => "is not included in the list",
+		'exclusion'    => "is reserved",
+		'invalid'      => "is invalid",
+		'confirmation' => "doesn't match confirmation",
+		'accepted'     => "must be accepted",
+		'empty'        => "can't be empty",
+		'blank'        => "can't be blank",
+		'too_long'     => "is too long (maximum is %d characters)",
+		'too_short'    => "is too short (minimum is %d characters)",
+		'wrong_length' => "is the wrong length (should be %d characters)",
+		'taken'        => "has already been taken",
+		'not_a_number' => "is not a number",
+		'greater_than' => "must be greater than %d",
+		'equal_to'     => "must be equal to %d",
+		'less_than'    => "must be less than %d",
+		'odd'          => "must be odd",
+		'even'         => "must be even",
+		'unique'       => "must be unique",
+		'less_than_or_equal_to' => "must be less than or equal to %d",
+		'greater_than_or_equal_to' => "must be greater than or equal to %d"
+	);
+
+	/**
+	 * Constructs an {@link Errors} object.
+	 *
+	 * @param Model $model The model the error is for
+	 * @return Errors
+	 */
+	public function __construct(Model $model)
+	{
+		$this->model = $model;
+	}
+
+	/**
+	 * Nulls $model so we don't get pesky circular references. $model is only needed during the
+	 * validation process and so can be safely cleared once that is done.
+	 */
+	public function clear_model()
+	{
+		$this->model = null;
+	}
+
+	/**
+	 * Add an error message.
+	 *
+	 * @param string $attribute Name of an attribute on the model
+	 * @param string $msg The error message
+	 */
+	public function add($attribute, $msg)
+	{
+		if (is_null($msg))
+			$msg = self :: $DEFAULT_ERROR_MESSAGES['invalid'];
+
+		if (!isset($this->errors[$attribute]))
+			$this->errors[$attribute] = array($msg);
+		else
+			$this->errors[$attribute][] = $msg;
+	}
+
+	/**
+	 * Adds an error message only if the attribute value is {@link http://www.php.net/empty empty}.
+	 *
+	 * @param string $attribute Name of an attribute on the model
+	 * @param string $msg The error message
+	 */
+	public function add_on_empty($attribute, $msg)
+	{
+		if (empty($msg))
+			$msg = self::$DEFAULT_ERROR_MESSAGES['empty'];
+
+		if (empty($this->model->$attribute))
+			$this->add($attribute, $msg);
+	}
+
+	/**
+	 * Retrieve error messages for an attribute.
+	 *
+	 * @param string $attribute Name of an attribute on the model
+	 * @return array or null if there is no error.
+	 */
+	public function __get($attribute)
+	{
+		if (!isset($this->errors[$attribute]))
+			return null;
+
+		return $this->errors[$attribute];
+	}
+
+	/**
+	 * Adds the error message only if the attribute value was null or an empty string.
+	 *
+	 * @param string $attribute Name of an attribute on the model
+	 * @param string $msg The error message
+	 */
+	public function add_on_blank($attribute, $msg)
+	{
+		if (!$msg)
+			$msg = self::$DEFAULT_ERROR_MESSAGES['blank'];
+
+		if (($value = $this->model->$attribute) === '' || $value === null)
+			$this->add($attribute, $msg);
+	}
+
+	/**
+	 * Returns true if the specified attribute had any error messages.
+	 *
+	 * @param string $attribute Name of an attribute on the model
+	 * @return boolean
+	 */
+	public function is_invalid($attribute)
+	{
+		return isset($this->errors[$attribute]);
+	}
+
+	/**
+	 * Returns the error message(s) for the specified attribute or null if none.
+	 *
+	 * @param string $attribute Name of an attribute on the model
+	 * @return string/array	Array of strings if several error occured on this attribute.
+	 */
+	public function on($attribute)
+	{
+		$errors = $this->$attribute;
+
+		return $errors && count($errors) == 1 ? $errors[0] : $errors;
+	}
+
+	/**
+	 * Returns the internal errors object.
+	 *
+	 * <code>
+	 * $model->errors->get_raw_errors();
+	 *
+	 * # array(
+	 * #  "name" => array("can't be blank"),
+	 * #  "state" => array("is the wrong length (should be 2 chars)",
+	 * # )
+	 * </code>
+	 */
+	public function get_raw_errors()
+	{
+		return $this->errors;
+	}
+
+	/**
+	 * Returns all the error messages as an array.
+	 *
+	 * <code>
+	 * $model->errors->full_messages();
+	 *
+	 * # array(
+	 * #  "Name can't be blank",
+	 * #  "State is the wrong length (should be 2 chars)"
+	 * # )
+	 * </code>
+	 *
+	 * @return array
+	 */
+	public function full_messages()
+	{
+		$full_messages = array();
+
+		$this->to_array(function($attribute, $message) use (&$full_messages) {
+			$full_messages[] = $message;
+		});
+
+		return $full_messages;
+	}
+
+	/**
+	 * Returns all the error messages as an array, including error key.
+	 *
+	 * <code>
+	 * $model->errors->errors();
+	 *
+	 * # array(
+	 * #  "name" => array("Name can't be blank"),
+	 * #  "state" => array("State is the wrong length (should be 2 chars)")
+	 * # )
+	 * </code>
+	 *
+	 * @param array $closure Closure to fetch the errors in some other format (optional)
+	 *                       This closure has the signature function($attribute, $message)
+	 *                       and is called for each available error message.
+	 * @return array
+	 */
+	public function to_array($closure=null)
+	{
+		$errors = array();
+
+		if ($this->errors)
+		{
+			foreach ($this->errors as $attribute => $messages)
+			{
+				foreach ($messages as $msg)
+				{
+					if (is_null($msg))
+						continue;
+
+					$errors[$attribute][] = ($message = Utils::human_attribute($attribute) . ' ' . $msg);
+
+					if ($closure)
+						$closure($attribute,$message);
+				}
+			}
+		}
+		return $errors;
+	}
+
+	/**
+	 * Convert all error messages to a String.
+	 * This function is called implicitely if the object is casted to a string:
+	 *
+	 * <code>
+	 * echo $error;
+	 *
+	 * # "Name can't be blank\nState is the wrong length (should be 2 chars)"
+	 * </code>
+	 * @return string
+	 */
+	public function __toString()
+	{
+		return implode("\n", $this->full_messages());
+	}
+
+	/**
+	 * Returns true if there are no error messages.
+	 * @return boolean
+	 */
+	public function is_empty()
+	{
+		return empty($this->errors);
+	}
+
+	/**
+	 * Clears out all error messages.
+	 */
+	public function clear()
+	{
+		$this->errors = array();
+	}
+
+	/**
+	 * Returns the number of error messages there are.
+	 * @return int
+	 */
+	public function size()
+	{
+		if ($this->is_empty())
+			return 0;
+
+		$count = 0;
+
+		foreach ($this->errors as $attribute => $error)
+			$count += count($error);
+
+		return $count;
+	}
+
+	/**
+	 * Returns an iterator to the error messages.
+	 *
+	 * This will allow you to iterate over the {@link Errors} object using foreach.
+	 *
+	 * <code>
+	 * foreach ($model->errors as $msg)
+	 *   echo "$msg\n";
+	 * </code>
+	 *
+	 * @return ArrayIterator
+	 */
+	public function getIterator()
+	{
+		return new ArrayIterator($this->full_messages());
+	}
+};
+?>

+ 99 - 0
hhvm/php-activerecord/lib/adapters/MysqlAdapter.php

@@ -0,0 +1,99 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Adapter for MySQL.
+ *
+ * @package ActiveRecord
+ */
+class MysqlAdapter extends Connection
+{
+	static $DEFAULT_PORT = 3306;
+
+	public function limit($sql, $offset, $limit)
+	{
+		$offset = is_null($offset) ? '' : intval($offset) . ',';
+		$limit = intval($limit);
+		return "$sql LIMIT {$offset}$limit";
+	}
+
+	public function query_column_info($table)
+	{
+		return $this->query("SHOW COLUMNS FROM $table");
+	}
+
+	public function query_for_tables()
+	{
+		return $this->query('SHOW TABLES');
+	}
+
+	public function create_column(&$column)
+	{
+		$c = new Column();
+		$c->inflected_name	= Inflector::instance()->variablize($column['field']);
+		$c->name			= $column['field'];
+		$c->nullable		= ($column['null'] === 'YES' ? true : false);
+		$c->pk				= ($column['key'] === 'PRI' ? true : false);
+		$c->auto_increment	= ($column['extra'] === 'auto_increment' ? true : false);
+
+		if ($column['type'] == 'timestamp' || $column['type'] == 'datetime')
+		{
+			$c->raw_type = 'datetime';
+			$c->length = 19;
+		}
+		elseif ($column['type'] == 'date')
+		{
+			$c->raw_type = 'date';
+			$c->length = 10;
+		}
+		elseif ($column['type'] == 'time')
+		{
+			$c->raw_type = 'time';
+			$c->length = 8;
+		}
+		else
+		{
+			preg_match('/^([A-Za-z0-9_]+)(\(([0-9]+(,[0-9]+)?)\))?/',$column['type'],$matches);
+
+			$c->raw_type = (count($matches) > 0 ? $matches[1] : $column['type']);
+
+			if (count($matches) >= 4)
+				$c->length = intval($matches[3]);
+		}
+
+		$c->map_raw_type();
+		$c->default = $c->cast($column['default'],$this);
+
+		return $c;
+	}
+
+	public function set_encoding($charset)
+	{
+		$params = array($charset);
+		$this->query('SET NAMES ?',$params);
+	}
+
+	public function accepts_limit_and_order_for_update_and_delete() { return true; }
+
+	public function native_database_types()
+	{
+		return array(
+			'primary_key' => 'int(11) UNSIGNED DEFAULT NULL auto_increment PRIMARY KEY',
+			'string' => array('name' => 'varchar', 'length' => 255),
+			'text' => array('name' => 'text'),
+			'integer' => array('name' => 'int', 'length' => 11),
+			'float' => array('name' => 'float'),
+			'datetime' => array('name' => 'datetime'),
+			'timestamp' => array('name' => 'datetime'),
+			'time' => array('name' => 'time'),
+			'date' => array('name' => 'date'),
+			'binary' => array('name' => 'blob'),
+			'boolean' => array('name' => 'tinyint', 'length' => 1)
+		);
+	}
+
+}
+?>

+ 146 - 0
hhvm/php-activerecord/lib/adapters/OciAdapter.php

@@ -0,0 +1,146 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+use PDO;
+
+/**
+ * Adapter for OCI (not completed yet).
+ * 
+ * @package ActiveRecord
+ */
+class OciAdapter extends Connection
+{
+	static $QUOTE_CHARACTER = '';
+	static $DEFAULT_PORT = 1521;
+
+	public $dsn_params;
+
+	protected function __construct($info)
+	{
+		try {
+			$this->dsn_params = isset($info->charset) ? ";charset=$info->charset" : "";
+			$this->connection = new PDO("oci:dbname=//$info->host/$info->db$this->dsn_params",$info->user,$info->pass,static::$PDO_OPTIONS);
+		} catch (PDOException $e) {
+			throw new DatabaseException($e);
+		}
+	}
+
+	public function supports_sequences() { return true; }
+	
+	public function get_next_sequence_value($sequence_name)
+	{
+		return $this->query_and_fetch_one('SELECT ' . $this->next_sequence_value($sequence_name) . ' FROM dual');
+	}
+
+	public function next_sequence_value($sequence_name)
+	{
+		return "$sequence_name.nextval";
+	}
+
+	public function date_to_string($datetime)
+	{
+		return $datetime->format('d-M-Y');
+	}
+
+	public function datetime_to_string($datetime)
+	{
+		return $datetime->format('d-M-Y h:i:s A');
+	}
+
+	// $string = DD-MON-YYYY HH12:MI:SS(\.[0-9]+) AM
+	public function string_to_datetime($string)
+	{
+		return parent::string_to_datetime(str_replace('.000000','',$string));
+	}
+
+	public function limit($sql, $offset, $limit)
+	{
+		$offset = intval($offset);
+		$stop = $offset + intval($limit);
+		return 
+			"SELECT * FROM (SELECT a.*, rownum ar_rnum__ FROM ($sql) a " .
+			"WHERE rownum <= $stop) WHERE ar_rnum__ > $offset";
+	}
+
+	public function query_column_info($table)
+	{
+		$sql = 
+			"SELECT c.column_name, c.data_type, c.data_length, c.data_scale, c.data_default, c.nullable, " .
+				"(SELECT a.constraint_type " .
+				"FROM all_constraints a, all_cons_columns b " .
+				"WHERE a.constraint_type='P' " .
+				"AND a.constraint_name=b.constraint_name " .
+				"AND a.table_name = t.table_name AND b.column_name=c.column_name) AS pk " .
+			"FROM user_tables t " .
+			"INNER JOIN user_tab_columns c on(t.table_name=c.table_name) " .
+			"WHERE t.table_name=?";
+
+		$values = array(strtoupper($table));
+		return $this->query($sql,$values);
+	}
+
+	public function query_for_tables()
+	{
+		return $this->query("SELECT table_name FROM user_tables");
+	}
+
+	public function create_column(&$column)
+	{
+		$column['column_name'] = strtolower($column['column_name']);
+		$column['data_type'] = strtolower(preg_replace('/\(.*?\)/','',$column['data_type']));
+
+		if ($column['data_default'] !== null)
+			$column['data_default'] = trim($column['data_default'],"' ");
+
+		if ($column['data_type'] == 'number')
+		{
+			if ($column['data_scale'] > 0)
+				$column['data_type'] = 'decimal';
+			elseif ($column['data_scale'] == 0)
+				$column['data_type'] = 'int';
+		}
+
+		$c = new Column();
+		$c->inflected_name	= Inflector::instance()->variablize($column['column_name']);
+		$c->name			= $column['column_name'];
+		$c->nullable		= $column['nullable'] == 'Y' ? true : false;
+		$c->pk				= $column['pk'] == 'P' ? true : false;
+		$c->length			= $column['data_length'];
+	
+		if ($column['data_type'] == 'timestamp')
+			$c->raw_type = 'datetime';
+		else
+			$c->raw_type = $column['data_type'];
+
+		$c->map_raw_type();
+		$c->default	= $c->cast($column['data_default'],$this);
+
+		return $c;
+	}
+
+	public function set_encoding($charset)
+	{
+		// is handled in the constructor
+	}
+
+	public function native_database_types()
+	{
+		return array(
+			'primary_key' => "NUMBER(38) NOT NULL PRIMARY KEY",
+			'string' => array('name' => 'VARCHAR2', 'length' => 255),
+			'text' => array('name' => 'CLOB'),
+			'integer' => array('name' => 'NUMBER', 'length' => 38),
+			'float' => array('name' => 'NUMBER'),
+			'datetime' => array('name' => 'DATE'),
+			'timestamp' => array('name' => 'DATE'),
+			'time' => array('name' => 'DATE'),
+			'date' => array('name' => 'DATE'),
+			'binary' => array('name' => 'BLOB'),
+			'boolean' => array('name' => 'NUMBER', 'length' => 1)
+		);
+	}
+}
+?>

+ 139 - 0
hhvm/php-activerecord/lib/adapters/PgsqlAdapter.php

@@ -0,0 +1,139 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+/**
+ * Adapter for Postgres (not completed yet)
+ * 
+ * @package ActiveRecord
+ */
+class PgsqlAdapter extends Connection
+{
+	static $QUOTE_CHARACTER = '"';
+	static $DEFAULT_PORT = 5432;
+
+	public function supports_sequences()
+	{
+		return true;
+	}
+
+	public function get_sequence_name($table, $column_name)
+	{
+		return "{$table}_{$column_name}_seq";
+	}
+
+	public function next_sequence_value($sequence_name)
+	{
+		return "nextval('" . str_replace("'","\\'",$sequence_name) . "')";
+	}
+
+	public function limit($sql, $offset, $limit)
+	{
+		return $sql . ' LIMIT ' . intval($limit) . ' OFFSET ' . intval($offset);
+	}
+
+	public function query_column_info($table)
+	{
+		$sql = <<<SQL
+SELECT
+      a.attname AS field,
+      a.attlen,
+      REPLACE(pg_catalog.format_type(a.atttypid, a.atttypmod), 'character varying', 'varchar') AS type,
+      a.attnotnull AS not_nullable,
+      (SELECT 't'
+        FROM pg_index
+        WHERE c.oid = pg_index.indrelid
+        AND a.attnum = ANY (pg_index.indkey)
+        AND pg_index.indisprimary = 't'
+      ) IS NOT NULL AS pk,      
+      REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE((SELECT pg_attrdef.adsrc
+        FROM pg_attrdef
+        WHERE c.oid = pg_attrdef.adrelid
+        AND pg_attrdef.adnum=a.attnum
+      ),'::[a-z_ ]+',''),'''$',''),'^''','') AS default
+FROM pg_attribute a, pg_class c, pg_type t
+WHERE c.relname = ?
+      AND a.attnum > 0
+      AND a.attrelid = c.oid
+      AND a.atttypid = t.oid
+ORDER BY a.attnum
+SQL;
+		$values = array($table);
+		return $this->query($sql,$values);
+	}
+
+	public function query_for_tables()
+	{
+		return $this->query("SELECT tablename FROM pg_tables WHERE schemaname NOT IN('information_schema','pg_catalog')");
+	}
+
+	public function create_column(&$column)
+	{
+		$c = new Column();
+		$c->inflected_name	= Inflector::instance()->variablize($column['field']);
+		$c->name			= $column['field'];
+		$c->nullable		= ($column['not_nullable'] ? false : true);
+		$c->pk				= ($column['pk'] ? true : false);
+		$c->auto_increment	= false;
+
+		if (substr($column['type'],0,9) == 'timestamp')
+		{
+			$c->raw_type = 'datetime';
+			$c->length = 19;
+		}
+		elseif ($column['type'] == 'date')
+		{
+			$c->raw_type = 'date';
+			$c->length = 10;
+		}
+		else
+		{
+			preg_match('/^([A-Za-z0-9_]+)(\(([0-9]+(,[0-9]+)?)\))?/',$column['type'],$matches);
+
+			$c->raw_type = (count($matches) > 0 ? $matches[1] : $column['type']);
+			$c->length = count($matches) >= 4 ? intval($matches[3]) : intval($column['attlen']);
+
+			if ($c->length < 0)
+				$c->length = null;
+		}
+
+		$c->map_raw_type();
+
+		if ($column['default'])
+		{
+			preg_match("/^nextval\('(.*)'\)$/",$column['default'],$matches);
+
+			if (count($matches) == 2)
+				$c->sequence = $matches[1];
+			else
+				$c->default = $c->cast($column['default'],$this);
+		}
+		return $c;
+	}
+
+	public function set_encoding($charset)
+	{
+		$this->query("SET NAMES '$charset'");
+	}
+
+	public function native_database_types()
+	{
+		return array(
+			'primary_key' => 'serial primary key',
+			'string' => array('name' => 'character varying', 'length' => 255),
+			'text' => array('name' => 'text'),
+			'integer' => array('name' => 'integer'),
+			'float' => array('name' => 'float'),
+			'datetime' => array('name' => 'datetime'),
+			'timestamp' => array('name' => 'timestamp'),
+			'time' => array('name' => 'time'),
+			'date' => array('name' => 'date'),
+			'binary' => array('name' => 'binary'),
+			'boolean' => array('name' => 'boolean')
+		);
+	}
+
+}
+?>

+ 110 - 0
hhvm/php-activerecord/lib/adapters/SqliteAdapter.php

@@ -0,0 +1,110 @@
+<?php
+/**
+ * @package ActiveRecord
+ */
+namespace ActiveRecord;
+
+use PDO;
+
+/**
+ * Adapter for SQLite.
+ *
+ * @package ActiveRecord
+ */
+class SqliteAdapter extends Connection
+{
+	protected function __construct($info)
+	{
+		if (!file_exists($info->host))
+			throw new DatabaseException("Could not find sqlite db: $info->host");
+
+		$this->connection = new PDO("sqlite:$info->host",null,null,static::$PDO_OPTIONS);
+	}
+
+	public function limit($sql, $offset, $limit)
+	{
+		$offset = is_null($offset) ? '' : intval($offset) . ',';
+		$limit = intval($limit);
+		return "$sql LIMIT {$offset}$limit";
+	}
+
+	public function query_column_info($table)
+	{
+		return $this->query("pragma table_info($table)");
+	}
+
+	public function query_for_tables()
+	{
+		return $this->query("SELECT name FROM sqlite_master");
+	}
+
+	public function create_column($column)
+	{
+		$c = new Column();
+		$c->inflected_name  = Inflector::instance()->variablize($column['name']);
+		$c->name            = $column['name'];
+		$c->nullable        = $column['notnull'] ? false : true;
+		$c->pk              = $column['pk'] ? true : false;
+		$c->auto_increment  = in_array(
+				strtoupper($column['type']),
+				array('INT', 'INTEGER')
+			) && $c->pk;
+
+		$column['type'] = preg_replace('/ +/',' ',$column['type']);
+		$column['type'] = str_replace(array('(',')'),' ',$column['type']);
+		$column['type'] = Utils::squeeze(' ',$column['type']);
+		$matches = explode(' ',$column['type']);
+
+		if (!empty($matches))
+		{
+			$c->raw_type = strtolower($matches[0]);
+
+			if (count($matches) > 1)
+				$c->length = intval($matches[1]);
+		}
+
+		$c->map_raw_type();
+
+		if ($c->type == Column::DATETIME)
+			$c->length = 19;
+		elseif ($c->type == Column::DATE)
+			$c->length = 10;
+
+		// From SQLite3 docs: The value is a signed integer, stored in 1, 2, 3, 4, 6,
+		// or 8 bytes depending on the magnitude of the value.
+		// so is it ok to assume it's possible an int can always go up to 8 bytes?
+		if ($c->type == Column::INTEGER && !$c->length)
+			$c->length = 8;
+
+		$c->default = $c->cast($column['dflt_value'],$this);
+
+		return $c;
+	}
+
+	public function set_encoding($charset)
+	{
+		throw new ActiveRecordException("SqliteAdapter::set_charset not supported.");
+	}
+
+	public function accepts_limit_and_order_for_update_and_delete() { return true; }
+
+	public function native_database_types()
+	{
+		return array(
+			'primary_key' => 'integer not null primary key',
+			'string' => array('name' => 'varchar', 'length' => 255),
+			'text' => array('name' => 'text'),
+			'integer' => array('name' => 'integer'),
+			'float' => array('name' => 'float'),
+			'decimal' => array('name' => 'decimal'),
+			'datetime' => array('name' => 'datetime'),
+			'timestamp' => array('name' => 'datetime'),
+			'time' => array('name' => 'time'),
+			'date' => array('name' => 'date'),
+			'binary' => array('name' => 'blob'),
+			'boolean' => array('name' => 'boolean')
+		);
+	}
+
+}
+?>

+ 45 - 0
hhvm/php-activerecord/lib/cache/Memcache.php

@@ -0,0 +1,45 @@
+<?php
+namespace ActiveRecord;
+
+class Memcache
+{
+	const DEFAULT_PORT = 11211;
+
+	private $memcache;
+
+	/**
+	 * Creates a Memcache instance.
+	 *
+	 * Takes an $options array w/ the following parameters:
+	 *
+	 * <ul>
+	 * <li><b>host:</b> host for the memcache server </li>
+	 * <li><b>port:</b> port for the memcache server </li>
+	 * </ul>
+	 * @param array $options
+	 */
+	public function __construct($options)
+	{
+		$this->memcache = new \Memcache();
+		$options['port'] = isset($options['port']) ? $options['port'] : self::DEFAULT_PORT;
+
+		if (!$this->memcache->connect($options['host'],$options['port']))
+			throw new CacheException("Could not connect to $options[host]:$options[port]");
+	}
+
+	public function flush()
+	{
+		$this->memcache->flush();
+	}
+
+	public function read($key)
+	{
+		return $this->memcache->get($key);
+	}
+
+	public function write($key, $value, $expire)
+	{
+		$this->memcache->set($key,$value,null,$expire);
+	}
+}
+?>

+ 46 - 0
hhvm/php-activerecord/test/ActiveRecordCacheTest.php

@@ -0,0 +1,46 @@
+<?php
+include 'helpers/config.php';
+
+use ActiveRecord\Cache;
+
+class ActiveRecordCacheTest extends DatabaseTest
+{
+	public function set_up($connection_name=null)
+	{
+		if (!extension_loaded('memcache'))
+		{
+			$this->markTestSkipped('The memcache extension is not available');
+			return;
+		}
+		
+		parent::set_up($connection_name);
+		ActiveRecord\Config::instance()->set_cache('memcache://localhost');
+	}
+
+	public function tear_down()
+	{
+		Cache::flush();
+		Cache::initialize(null);
+	}
+
+	public function test_default_expire()
+	{
+		$this->assert_equals(30,Cache::$options['expire']);
+	}
+
+	public function test_explicit_default_expire()
+	{
+		ActiveRecord\Config::instance()->set_cache('memcache://localhost',array('expire' => 1));
+		$this->assert_equals(1,Cache::$options['expire']);
+	}
+
+	public function test_caches_column_meta_data()
+	{
+		Author::first();
+
+		$table_name = Author::table()->get_fully_qualified_table_name(!($this->conn instanceof ActiveRecord\PgsqlAdapter));
+		$value = Cache::$adapter->read("get_meta_data-$table_name");
+		$this->assert_true(is_array($value));
+	}
+}
+?>

+ 464 - 0
hhvm/php-activerecord/test/ActiveRecordFindTest.php

@@ -0,0 +1,464 @@
+<?php
+include 'helpers/config.php';
+
+class ActiveRecordFindTest extends DatabaseTest
+{
+	/**
+	 * @expectedException ActiveRecord\RecordNotFound
+	 */
+	public function test_find_with_no_params()
+	{
+		Author::find();
+	}
+
+	public function test_find_by_pk()
+	{
+		$author = Author::find(3);
+		$this->assert_equals(3,$author->id);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\RecordNotFound
+	 */
+	public function test_find_by_pkno_results()
+	{
+		Author::find(99999999);
+	}
+
+	public function test_find_by_multiple_pk_with_partial_match()
+	{
+		try
+		{
+			Author::find(1,999999999);
+			$this->fail();
+		}
+		catch (ActiveRecord\RecordNotFound $e)
+		{
+			$this->assert_true(strpos($e->getMessage(),'found 1, but was looking for 2') !== false);
+		}
+	}
+
+	public function test_find_by_pk_with_options()
+	{
+		$author = Author::find(3,array('order' => 'name'));
+		$this->assert_equals(3,$author->id);
+		$this->assert_true(strpos(Author::table()->last_sql,'ORDER BY name') !== false);
+	}
+
+	public function test_find_by_pk_array()
+	{
+		$authors = Author::find(1,'2');
+		$this->assert_equals(2, count($authors));
+		$this->assert_equals(1, $authors[0]->id);
+		$this->assert_equals(2, $authors[1]->id);
+	}
+
+	public function test_find_by_pk_array_with_options()
+	{
+		$authors = Author::find(1,'2',array('order' => 'name'));
+		$this->assert_equals(2, count($authors));
+		$this->assert_true(strpos(Author::table()->last_sql,'ORDER BY name') !== false);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\RecordNotFound
+	 */
+	public function test_find_nothing_with_sql_in_string()
+	{
+		Author::first('name = 123123123');
+	}
+
+	public function test_find_all()
+	{
+		$authors = Author::find('all',array('conditions' => array('author_id IN(?)',array(1,2,3))));
+		$this->assert_true(count($authors) >= 3);
+	}
+
+	public function test_find_all_with_no_bind_values()
+	{
+		$authors = Author::find('all',array('conditions' => array('author_id IN(1,2,3)')));
+		$this->assert_equals(1,$authors[0]->author_id);
+	}
+
+	public function test_find_hash_using_alias()
+	{
+		$venues = Venue::all(array('conditions' => array('marquee' => 'Warner Theatre', 'city' => array('Washington','New York'))));
+		$this->assert_true(count($venues) >= 1);
+	}
+
+	public function test_find_hash_using_alias_with_null()
+	{
+		$venues = Venue::all(array('conditions' => array('marquee' => null)));
+		$this->assert_equals(0,count($venues));
+	}
+
+	public function test_dynamic_finder_using_alias()
+	{
+		$this->assert_not_null(Venue::find_by_marquee('Warner Theatre'));
+	}
+
+	public function test_find_all_hash()
+	{
+		$books = Book::find('all',array('conditions' => array('author_id' => 1)));
+		$this->assert_true(count($books) > 0);
+	}
+
+	public function test_find_all_hash_with_order()
+	{
+		$books = Book::find('all',array('conditions' => array('author_id' => 1), 'order' => 'name DESC'));
+		$this->assert_true(count($books) > 0);
+	}
+
+	public function test_find_all_no_args()
+	{
+		$author = Author::all();
+		$this->assert_true(count($author) > 1);
+	}
+
+	public function test_find_all_no_results()
+	{
+		$authors = Author::find('all',array('conditions' => array('author_id IN(11111111111,22222222222,333333333333)')));
+		$this->assert_equals(array(),$authors);
+	}
+
+	public function test_find_first()
+	{
+		$author = Author::find('first',array('conditions' => array('author_id IN(?)', array(1,2,3))));
+		$this->assert_equals(1,$author->author_id);
+		$this->assert_equals('Tito',$author->name);
+	}
+
+	public function test_find_first_no_results()
+	{
+		$this->assert_null(Author::find('first',array('conditions' => 'author_id=1111111')));
+	}
+
+	public function test_find_first_using_pk()
+	{
+		$author = Author::find('first',3);
+		$this->assert_equals(3,$author->author_id);
+	}
+
+	public function test_find_first_with_conditions_as_string()
+	{
+		$author = Author::find('first',array('conditions' => 'author_id=3'));
+		$this->assert_equals(3,$author->author_id);
+	}
+
+	public function test_find_all_with_conditions_as_string()
+	{
+		$author = Author::find('all',array('conditions' => 'author_id in(2,3)'));
+		$this->assert_equals(2,count($author));
+	}
+
+	public function test_find_by_sql()
+	{
+		$author = Author::find_by_sql("SELECT * FROM authors WHERE author_id in(1,2)");
+		$this->assert_equals(1,$author[0]->author_id);
+		$this->assert_equals(2,count($author));
+	}
+
+	public function test_find_by_sqltakes_values_array()
+	{
+		$author = Author::find_by_sql("SELECT * FROM authors WHERE author_id=?",array(1));
+		$this->assert_not_null($author);
+	}
+
+	public function test_find_with_conditions()
+	{
+		$author = Author::find(array('conditions' => array('author_id=? and name=?', 1, 'Tito')));
+		$this->assert_equals(1,$author->author_id);
+	}
+
+	public function test_find_last()
+	{
+		$author = Author::last();
+		$this->assert_equals(4, $author->author_id);
+		$this->assert_equals('Uncle Bob',$author->name);
+	}
+
+	public function test_find_last_using_string_condition()
+	{
+		$author = Author::find('last', array('conditions' => 'author_id IN(1,2,3,4)'));
+		$this->assert_equals(4, $author->author_id);
+		$this->assert_equals('Uncle Bob',$author->name);
+	}
+
+	public function test_limit_before_order()
+	{
+		$authors = Author::all(array('limit' => 2, 'order' => 'author_id desc', 'conditions' => 'author_id in(1,2)'));
+		$this->assert_equals(2,$authors[0]->author_id);
+		$this->assert_equals(1,$authors[1]->author_id);
+	}
+
+	public function test_for_each()
+	{
+		$i = 0;
+		$res = Author::all();
+
+		foreach ($res as $author)
+		{
+			$this->assert_true($author instanceof ActiveRecord\Model);
+			$i++;
+		}
+		$this->assert_true($i > 0);
+	}
+
+	public function test_fetch_all()
+	{
+		$i = 0;
+
+		foreach (Author::all() as $author)
+		{
+			$this->assert_true($author instanceof ActiveRecord\Model);
+			$i++;
+		}
+		$this->assert_true($i > 0);
+	}
+
+	public function test_count()
+	{
+		$this->assert_equals(1,Author::count(1));
+		$this->assert_equals(2,Author::count(array(1,2)));
+		$this->assert_true(Author::count() > 1);
+		$this->assert_equals(0,Author::count(array('conditions' => 'author_id=99999999999999')));
+		$this->assert_equals(2,Author::count(array('conditions' => 'author_id=1 or author_id=2')));
+		$this->assert_equals(1,Author::count(array('name' => 'Tito', 'author_id' => 1)));
+	}
+
+	public function test_gh149_empty_count()
+	{
+		$total = Author::count();
+		$this->assert_equals($total, Author::count(null));
+		$this->assert_equals($total, Author::count(array()));
+	}
+
+	public function test_exists()
+	{
+		$this->assert_true(Author::exists(1));
+		$this->assert_true(Author::exists(array('conditions' => 'author_id=1')));
+		$this->assert_true(Author::exists(array('conditions' => array('author_id=? and name=?', 1, 'Tito'))));
+		$this->assert_false(Author::exists(9999999));
+		$this->assert_false(Author::exists(array('conditions' => 'author_id=999999')));
+	}
+
+	public function test_find_by_call_static()
+	{
+		$this->assert_equals('Tito',Author::find_by_name('Tito')->name);
+		$this->assert_equals('Tito',Author::find_by_author_id_and_name(1,'Tito')->name);
+		$this->assert_equals('George W. Bush',Author::find_by_author_id_or_name(2,'Tito',array('order' => 'author_id desc'))->name);
+		$this->assert_equals('Tito',Author::find_by_name(array('Tito','George W. Bush'),array('order' => 'name desc'))->name);
+	}
+
+	public function test_find_by_call_static_no_results()
+	{
+		$this->assert_null(Author::find_by_name('SHARKS WIT LASERZ'));
+		$this->assert_null(Author::find_by_name_or_author_id());
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_find_by_call_static_invalid_column_name()
+	{
+		Author::find_by_sharks();
+	}
+
+	public function test_find_all_by_call_static()
+	{
+		$x = Author::find_all_by_name('Tito');
+		$this->assert_equals('Tito',$x[0]->name);
+		$this->assert_equals(1,count($x));
+
+		$x = Author::find_all_by_author_id_or_name(2,'Tito',array('order' => 'name asc'));
+		$this->assert_equals(2,count($x));
+		$this->assert_equals('George W. Bush',$x[0]->name);
+	}
+
+	public function test_find_all_by_call_static_no_results()
+	{
+		$x = Author::find_all_by_name('SHARKSSSSSSS');
+		$this->assert_equals(0,count($x));
+	}
+
+	public function test_find_all_by_call_static_with_array_values_and_options()
+	{
+		$author = Author::find_all_by_name(array('Tito','Bill Clinton'),array('order' => 'name desc'));
+		$this->assert_equals('Tito',$author[0]->name);
+		$this->assert_equals('Bill Clinton',$author[1]->name);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_find_all_by_call_static_undefined_method()
+	{
+		Author::find_sharks('Tito');
+	}
+
+	public function test_find_all_takes_limit_options()
+	{
+		$authors = Author::all(array('limit' => 1, 'offset' => 2, 'order' => 'name desc'));
+		$this->assert_equals('George W. Bush',$authors[0]->name);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_find_by_call_static_with_invalid_field_name()
+	{
+		Author::find_by_some_invalid_field_name('Tito');
+	}
+
+	public function test_find_with_select()
+	{
+		$author = Author::first(array('select' => 'name, 123 as bubba', 'order' => 'name desc'));
+		$this->assert_equals('Uncle Bob',$author->name);
+		$this->assert_equals(123,$author->bubba);
+	}
+
+	public function test_find_with_select_non_selected_fields_should_not_have_attributes()
+	{
+		$author = Author::first(array('select' => 'name, 123 as bubba'));
+		try {
+			$author->id;
+			$this->fail('expected ActiveRecord\UndefinedPropertyExecption');
+		} catch (ActiveRecord\UndefinedPropertyException $e) {
+			;
+		}
+	}
+
+	public function test_joins_on_model_with_association_and_explicit_joins()
+	{
+		JoinBook::$belongs_to = array(array('author'));
+		JoinBook::first(array('joins' => array('author','LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)')));
+		$this->assert_sql_has('INNER JOIN authors ON(books.author_id = authors.author_id)',JoinBook::table()->last_sql);
+		$this->assert_sql_has('LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)',JoinBook::table()->last_sql);
+	}
+
+	public function test_joins_on_model_with_explicit_joins()
+	{
+		JoinBook::first(array('joins' => array('LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)')));
+		$this->assert_sql_has('LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)',JoinBook::table()->last_sql);
+	}
+
+	public function test_group()
+	{
+		$venues = Venue::all(array('select' => 'state', 'group' => 'state'));
+		$this->assert_true(count($venues) > 0);
+		$this->assert_sql_has('GROUP BY state',ActiveRecord\Table::load('Venue')->last_sql);
+	}
+
+	public function test_group_with_order_and_limit_and_having()
+	{
+		$venues = Venue::all(array('select' => 'state', 'group' => 'state', 'having' => 'length(state) = 2', 'order' => 'state', 'limit' => 2));
+		$this->assert_true(count($venues) > 0);
+		$this->assert_sql_has($this->conn->limit('SELECT state FROM venues GROUP BY state HAVING length(state) = 2 ORDER BY state',null,2),Venue::table()->last_sql);
+	}
+
+	public function test_escape_quotes()
+	{
+		$author = Author::find_by_name("Tito's");
+		$this->assert_not_equals("Tito's",Author::table()->last_sql);
+	}
+
+	public function test_from()
+	{
+		$author = Author::find('first', array('from' => 'books', 'order' => 'author_id asc'));
+		$this->assert_true($author instanceof Author);
+		$this->assert_not_null($author->book_id);
+
+		$author = Author::find('first', array('from' => 'authors', 'order' => 'author_id asc'));
+		$this->assert_true($author instanceof Author);
+		$this->assert_equals(1, $author->id);
+	}
+
+	public function test_having()
+	{
+		if ($this->conn instanceof ActiveRecord\OciAdapter)
+		{
+			$author = Author::first(array(
+				'select' => 'to_char(created_at,\'YYYY-MM-DD\') as created_at',
+				'group'  => 'to_char(created_at,\'YYYY-MM-DD\')',
+				'having' => "to_char(created_at,'YYYY-MM-DD') > '2009-01-01'"));
+			$this->assert_sql_has("GROUP BY to_char(created_at,'YYYY-MM-DD') HAVING to_char(created_at,'YYYY-MM-DD') > '2009-01-01'",Author::table()->last_sql);
+		}
+		else
+		{
+			$author = Author::first(array(
+				'select' => 'date(created_at) as created_at',
+				'group'  => 'date(created_at)',
+				'having' => "date(created_at) > '2009-01-01'"));
+			$this->assert_sql_has("GROUP BY date(created_at) HAVING date(created_at) > '2009-01-01'",Author::table()->last_sql);
+		}
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_from_with_invalid_table()
+	{
+		$author = Author::find('first', array('from' => 'wrong_authors_table'));
+	}
+
+	public function test_find_with_hash()
+	{
+		$this->assert_not_null(Author::find(array('name' => 'Tito')));
+		$this->assert_not_null(Author::find('first',array('name' => 'Tito')));
+		$this->assert_equals(1,count(Author::find('all',array('name' => 'Tito'))));
+		$this->assert_equals(1,count(Author::all(array('name' => 'Tito'))));
+	}
+
+	public function test_find_or_create_by_on_existing_record()
+	{
+		$this->assert_not_null(Author::find_or_create_by_name('Tito'));
+	}
+
+	public function test_find_or_create_by_creates_new_record()
+	{
+		$author = Author::find_or_create_by_name_and_encrypted_password('New Guy','pencil');
+		$this->assert_true($author->author_id > 0);
+		$this->assert_equals('pencil',$author->encrypted_password);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_find_or_create_by_throws_exception_when_using_or()
+	{
+		Author::find_or_create_by_name_or_encrypted_password('New Guy','pencil');
+	}
+
+	/**
+	 * @expectedException ActiveRecord\RecordNotFound
+	 */
+	public function test_find_by_zero()
+	{
+		Author::find(0);
+	}
+
+	public function test_count_by()
+	{
+		$this->assert_equals(2,Venue::count_by_state('VA'));
+		$this->assert_equals(3,Venue::count_by_state_or_name('VA','Warner Theatre'));
+		$this->assert_equals(0,Venue::count_by_state_and_name('VA','zzzzzzzzzzzzz'));
+	}
+
+	public function test_find_by_pk_should_not_use_limit()
+	{
+		Author::find(1);
+		$this->assert_sql_has('SELECT * FROM authors WHERE author_id=?',Author::table()->last_sql);
+	}
+
+	public function test_find_by_datetime()
+	{
+		$now = new DateTime();
+		$arnow = new ActiveRecord\DateTime();
+		$arnow->setTimestamp($now->getTimestamp());
+
+		Author::find(1)->update_attribute('created_at',$now);
+		$this->assert_not_null(Author::find_by_created_at($now));
+		$this->assert_not_null(Author::find_by_created_at($arnow));
+	}
+};
+?>

+ 523 - 0
hhvm/php-activerecord/test/ActiveRecordTest.php

@@ -0,0 +1,523 @@
+<?php
+include 'helpers/config.php';
+
+class ActiveRecordTest extends DatabaseTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+		$this->options = array('conditions' => 'blah', 'order' => 'blah');
+	}
+
+	public function test_options_is_not()
+	{
+		$this->assert_false(Author::is_options_hash(null));
+		$this->assert_false(Author::is_options_hash(''));
+		$this->assert_false(Author::is_options_hash('tito'));
+		$this->assert_false(Author::is_options_hash(array()));
+		$this->assert_false(Author::is_options_hash(array(1,2,3)));
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_options_hash_with_unknown_keys() {
+		$this->assert_false(Author::is_options_hash(array('conditions' => 'blah', 'sharks' => 'laserz', 'dubya' => 'bush')));
+	}
+
+	public function test_options_is_hash()
+	{
+		$this->assert_true(Author::is_options_hash($this->options));
+	}
+
+	public function test_extract_and_validate_options() {
+		$args = array('first',$this->options);
+		$this->assert_equals($this->options,Author::extract_and_validate_options($args));
+		$this->assert_equals(array('first'),$args);
+	}
+
+	public function test_extract_and_validate_options_with_array_in_args() {
+		$args = array('first',array(1,2),$this->options);
+		$this->assert_equals($this->options,Author::extract_and_validate_options($args));
+	}
+
+	public function test_extract_and_validate_options_removes_options_hash() {
+		$args = array('first',$this->options);
+		Author::extract_and_validate_options($args);
+		$this->assert_equals(array('first'),$args);
+	}
+
+	public function test_extract_and_validate_options_nope() {
+		$args = array('first');
+		$this->assert_equals(array(),Author::extract_and_validate_options($args));
+		$this->assert_equals(array('first'),$args);
+	}
+
+	public function test_extract_and_validate_options_nope_because_wasnt_at_end() {
+		$args = array('first',$this->options,array(1,2));
+		$this->assert_equals(array(),Author::extract_and_validate_options($args));
+	}
+
+	/**
+	 * @expectedException ActiveRecord\UndefinedPropertyException
+	 */
+	public function test_invalid_attribute()
+	{
+		$author = Author::find('first',array('conditions' => 'author_id=1'));
+		$author->some_invalid_field_name;
+	}
+
+	public function test_invalid_attributes()
+	{
+		$book = Book::find(1);
+		try {
+			$book->update_attributes(array('name' => 'new name', 'invalid_attribute' => true , 'another_invalid_attribute' => 'something'));
+		} catch (ActiveRecord\UndefinedPropertyException $e) {
+			$exceptions = explode("\r\n", $e->getMessage());
+		}
+
+		$this->assert_equals(1, substr_count($exceptions[0], 'invalid_attribute'));
+		$this->assert_equals(1, substr_count($exceptions[1], 'another_invalid_attribute'));
+	}
+
+	public function test_getter_undefined_property_exception_includes_model_name()
+	{
+		$this->assert_exception_message_contains("Author->this_better_not_exist",function()
+		{
+			$author = new Author();
+			$author->this_better_not_exist;
+		});
+	}
+
+	public function test_mass_assignment_undefined_property_exception_includes_model_name()
+	{
+		$this->assert_exception_message_contains("Author->this_better_not_exist",function()
+		{
+			new Author(array("this_better_not_exist" => "hi"));
+		});
+	}
+
+	public function test_setter_undefined_property_exception_includes_model_name()
+	{
+		$this->assert_exception_message_contains("Author->this_better_not_exist",function()
+		{
+			$author = new Author();
+			$author->this_better_not_exist = "hi";
+		});
+	}
+
+	public function test_get_values_for()
+	{
+		$book = Book::find_by_name('Ancient Art of Main Tanking');
+		$ret = $book->get_values_for(array('book_id','author_id'));
+		$this->assert_equals(array('book_id','author_id'),array_keys($ret));
+		$this->assert_equals(array(1,1),array_values($ret));
+	}
+
+	public function test_hyphenated_column_names_to_underscore()
+	{
+		if ($this->conn instanceof ActiveRecord\OciAdapter)
+			return;
+
+		$keys = array_keys(RmBldg::first()->attributes());
+		$this->assert_true(in_array('rm_name',$keys));
+	}
+
+	public function test_column_names_with_spaces()
+	{
+		if ($this->conn instanceof ActiveRecord\OciAdapter)
+			return;
+
+		$keys = array_keys(RmBldg::first()->attributes());
+		$this->assert_true(in_array('space_out',$keys));
+	}
+
+	public function test_mixed_case_column_name()
+	{
+		$keys = array_keys(Author::first()->attributes());
+		$this->assert_true(in_array('mixedcasefield',$keys));
+	}
+
+	public function test_mixed_case_primary_key_save()
+	{
+		$venue = Venue::find(1);
+		$venue->name = 'should not throw exception';
+		$venue->save();
+		$this->assert_equals($venue->name,Venue::find(1)->name);
+	}
+
+	public function test_reload()
+	{
+		$venue = Venue::find(1);
+		$this->assert_equals('NY', $venue->state);
+		$venue->state = 'VA';
+		$this->assert_equals('VA', $venue->state);
+		$venue->reload();
+		$this->assert_equals('NY', $venue->state);
+	}
+	
+	public function test_reload_protected_attribute()
+	{
+		$book = BookAttrAccessible::find(1);
+	
+		$book->name = "Should not stay";
+		$book->reload();
+		$this->assert_not_equals("Should not stay", $book->name);
+	}
+
+	public function test_active_record_model_home_not_set()
+	{
+		$home = ActiveRecord\Config::instance()->get_model_directory();
+		ActiveRecord\Config::instance()->set_model_directory(__FILE__);
+		$this->assert_equals(false,class_exists('TestAutoload'));
+
+		ActiveRecord\Config::instance()->set_model_directory($home);
+	}
+
+	public function test_auto_load_with_namespaced_model()
+	{
+		$this->assert_true(class_exists('NamespaceTest\Book'));
+	}
+
+	public function test_namespace_gets_stripped_from_table_name()
+	{
+		$model = new NamespaceTest\Book();
+		$this->assert_equals('books',$model->table()->table);
+	}
+
+	public function test_namespace_gets_stripped_from_inferred_foreign_key()
+	{
+		$model = new NamespaceTest\Book();
+		$table = ActiveRecord\Table::load(get_class($model));
+		$this->assert_equals($table->get_relationship('parent_book')->foreign_key[0], 'book_id');
+	}
+
+	public function test_should_have_all_column_attributes_when_initializing_with_array()
+	{
+		$author = new Author(array('name' => 'Tito'));
+		$this->assert_true(count(array_keys($author->attributes())) >= 9);
+	}
+
+	public function test_defaults()
+	{
+		$author = new Author();
+		$this->assert_equals('default_name',$author->name);
+	}
+
+	public function test_alias_attribute_getter()
+	{
+		$venue = Venue::find(1);
+		$this->assert_equals($venue->marquee, $venue->name);
+		$this->assert_equals($venue->mycity, $venue->city);
+	}
+
+	public function test_alias_attribute_setter()
+	{
+		$venue = Venue::find(1);
+		$venue->marquee = 'new name';
+		$this->assert_equals($venue->marquee, 'new name');
+		$this->assert_equals($venue->marquee, $venue->name);
+
+		$venue->name = 'another name';
+		$this->assert_equals($venue->name, 'another name');
+		$this->assert_equals($venue->marquee, $venue->name);
+	}
+
+	public function test_alias_from_mass_attributes()
+	{
+		$venue = new Venue(array('marquee' => 'meme', 'id' => 123));
+		$this->assert_equals('meme',$venue->name);
+		$this->assert_equals($venue->marquee,$venue->name);
+	}
+
+	public function test_gh18_isset_on_aliased_attribute()
+	{
+		$this->assert_true(isset(Venue::first()->marquee));
+	}
+
+	public function test_attr_accessible()
+	{
+		$book = new BookAttrAccessible(array('name' => 'should not be set', 'author_id' => 1));
+		$this->assert_null($book->name);
+		$this->assert_equals(1,$book->author_id);
+		$book->name = 'test';
+		$this->assert_equals('test', $book->name);
+	}
+
+	public function test_attr_protected()
+	{
+		$book = new BookAttrAccessible(array('book_id' => 999));
+		$this->assert_null($book->book_id);
+		$book->book_id = 999;
+		$this->assert_equals(999, $book->book_id);
+	}
+
+	public function test_isset()
+	{
+		$book = new Book();
+		$this->assert_true(isset($book->name));
+		$this->assert_false(isset($book->sharks));
+	}
+
+	public function test_readonly_only_halt_on_write_method()
+	{
+		$book = Book::first(array('readonly' => true));
+		$this->assert_true($book->is_readonly());
+
+		try {
+			$book->save();
+			$this-fail('expected exception ActiveRecord\ReadonlyException');
+		} catch (ActiveRecord\ReadonlyException $e) {
+		}
+
+		$book->name = 'some new name';
+		$this->assert_equals($book->name, 'some new name');
+	}
+
+	public function test_cast_when_using_setter()
+	{
+		$book = new Book();
+		$book->book_id = '1';
+		$this->assert_same(1,$book->book_id);
+	}
+
+	public function test_cast_when_loading()
+	{
+		$book = Book::find(1);
+		$this->assert_same(1,$book->book_id);
+		$this->assert_same('Ancient Art of Main Tanking',$book->name);
+	}
+
+	public function test_cast_defaults()
+	{
+		$book = new Book();
+		$this->assert_same(0.0,$book->special);
+	}
+
+	public function test_transaction_committed()
+	{
+		$original = Author::count();
+		$ret = Author::transaction(function() { Author::create(array("name" => "blah")); });
+		$this->assert_equals($original+1,Author::count());
+		$this->assert_true($ret);
+	}
+	
+	public function test_transaction_committed_when_returning_true()
+	{
+		$original = Author::count();
+		$ret = Author::transaction(function() { Author::create(array("name" => "blah")); return true; });
+		$this->assert_equals($original+1,Author::count());
+		$this->assert_true($ret);
+	}
+	
+	public function test_transaction_rolledback_by_returning_false()
+	{
+		$original = Author::count();
+		
+		$ret = Author::transaction(function()
+		{
+			Author::create(array("name" => "blah"));
+			return false;
+		});
+		
+		$this->assert_equals($original,Author::count());
+		$this->assert_false($ret);
+	}
+	
+	public function test_transaction_rolledback_by_throwing_exception()
+	{
+		$original = Author::count();
+		$exception = null;
+
+		try
+		{
+			Author::transaction(function()
+			{
+				Author::create(array("name" => "blah"));
+				throw new Exception("blah");
+			});
+		}
+		catch (Exception $e)
+		{
+			$exception = $e;
+		}
+
+		$this->assert_not_null($exception);
+		$this->assert_equals($original,Author::count());
+	}
+
+	public function test_delegate()
+	{
+		$event = Event::first();
+		$this->assert_equals($event->venue->state,$event->state);
+		$this->assert_equals($event->venue->address,$event->address);
+	}
+
+	public function test_delegate_prefix()
+	{
+		$event = Event::first();
+		$this->assert_equals($event->host->name,$event->woot_name);
+	}
+
+	public function test_delegate_returns_null_if_relationship_does_not_exist()
+	{
+		$event = new Event();
+		$this->assert_null($event->state);
+	}
+
+	public function test_delegate_set_attribute()
+	{
+		$event = Event::first();
+		$event->state = 'MEXICO';
+		$this->assert_equals('MEXICO',$event->venue->state);
+	}
+
+	public function test_delegate_getter_gh_98()
+	{
+		Venue::$use_custom_get_state_getter = true;
+
+		$event = Event::first();
+		$this->assert_equals('ny', $event->venue->state);
+		$this->assert_equals('ny', $event->state);
+
+		Venue::$use_custom_get_state_getter = false;
+	}
+
+	public function test_delegate_setter_gh_98()
+	{
+		Venue::$use_custom_set_state_setter = true;
+
+		$event = Event::first();
+		$event->state = 'MEXICO';
+		$this->assert_equals('MEXICO#',$event->venue->state);
+
+		Venue::$use_custom_set_state_setter = false;
+	}
+
+	public function test_table_name_with_underscores()
+	{
+		$this->assert_not_null(AwesomePerson::first());
+	}
+
+	public function test_model_should_default_as_new_record()
+	{
+		$author = new Author();
+		$this->assert_true($author->is_new_record());
+	}
+
+	public function test_setter()
+	{
+		$author = new Author();
+		$author->password = 'plaintext';
+		$this->assert_equals(md5('plaintext'),$author->encrypted_password);
+	}
+
+	public function test_setter_with_same_name_as_an_attribute()
+	{
+		$author = new Author();
+		$author->name = 'bob';
+		$this->assert_equals('BOB',$author->name);
+	}
+
+	public function test_getter()
+	{
+		$book = Book::first();
+		$this->assert_equals(strtoupper($book->name), $book->upper_name);
+	}
+
+	public function test_getter_with_same_name_as_an_attribute()
+	{
+		Book::$use_custom_get_name_getter = true;
+		$book = new Book;
+		$book->name = 'bob';
+		$this->assert_equals('BOB', $book->name);
+		Book::$use_custom_get_name_getter = false;
+	}
+
+	public function test_setting_invalid_date_should_set_date_to_null()
+	{
+		$author = new Author();
+		$author->created_at = 'CURRENT_TIMESTAMP';
+		$this->assertNull($author->created_at);
+	}
+
+	public function test_table_name()
+	{
+		$this->assert_equals('authors',Author::table_name());
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_undefined_instance_method()
+	{
+		Author::first()->find_by_name('sdf');
+	}
+
+	public function test_clear_cache_for_specific_class()
+	{
+		$book_table1 = ActiveRecord\Table::load('Book');
+		$book_table2 = ActiveRecord\Table::load('Book');
+		ActiveRecord\Table::clear_cache('Book');
+		$book_table3 = ActiveRecord\Table::load('Book');
+
+		$this->assert_true($book_table1 === $book_table2);
+		$this->assert_true($book_table1 !== $book_table3);
+	}
+
+	public function test_flag_dirty()
+	{
+		$author = new Author();
+		$author->flag_dirty('some_date');
+		$this->assert_has_keys('some_date', $author->dirty_attributes());
+		$this->assert_true($author->attribute_is_dirty('some_date'));
+		$author->save();
+		$this->assert_false($author->attribute_is_dirty('some_date'));
+	}
+	
+	public function test_flag_dirty_attribute()
+	{
+		$author = new Author();
+		$author->flag_dirty('some_inexistant_property');
+		$this->assert_null($author->dirty_attributes());
+		$this->assert_false($author->attribute_is_dirty('some_inexistant_property'));
+	}
+	
+	public function test_assigning_php_datetime_gets_converted_to_ar_datetime()
+	{
+		$author = new Author();
+		$author->created_at = $now = new \DateTime();
+		$this->assert_is_a("ActiveRecord\\DateTime",$author->created_at);
+		$this->assert_datetime_equals($now,$author->created_at);
+	}
+
+	public function test_assigning_from_mass_assignment_php_datetime_gets_converted_to_ar_datetime()
+	{
+		$author = new Author(array('created_at' => new \DateTime()));
+		$this->assert_is_a("ActiveRecord\\DateTime",$author->created_at);
+	}
+
+	public function test_get_real_attribute_name()
+	{
+		$venue = new Venue();
+		$this->assert_equals('name', $venue->get_real_attribute_name('name'));
+		$this->assert_equals('name', $venue->get_real_attribute_name('marquee'));
+		$this->assert_equals(null, $venue->get_real_attribute_name('invalid_field'));
+	}
+
+	public function test_id_setter_works_with_table_without_pk_named_attribute()
+	{
+		$author = new Author(array('id' => 123));
+		$this->assert_equals(123,$author->author_id);
+	}
+
+	public function test_query()
+	{
+		$row = Author::query('SELECT COUNT(*) AS n FROM authors',null)->fetch();
+		$this->assert_true($row['n'] > 1);
+
+		$row = Author::query('SELECT COUNT(*) AS n FROM authors WHERE name=?',array('Tito'))->fetch();
+		$this->assert_equals(array('n' => 1), $row);
+	}
+};
+?>

+ 428 - 0
hhvm/php-activerecord/test/ActiveRecordWriteTest.php

@@ -0,0 +1,428 @@
+<?php
+include 'helpers/config.php';
+use ActiveRecord\DateTime;
+
+class DirtyAuthor extends ActiveRecord\Model
+{
+	static $table = 'authors';
+	static $before_save = 'before_save';
+
+	public function before_save()
+	{
+		$this->name = 'i saved';
+	}
+};
+
+class AuthorWithoutSequence extends ActiveRecord\Model
+{
+	static $table = 'authors';
+	static $sequence = 'invalid_seq';
+}
+
+class AuthorExplicitSequence extends ActiveRecord\Model
+{
+	static $sequence = 'blah_seq';
+}
+
+class ActiveRecordWriteTest extends DatabaseTest
+{
+	private function make_new_book_and($save=true)
+	{
+		$book = new Book();
+		$book->name = 'rivers cuomo';
+		$book->special = 1;
+
+		if ($save)
+			$book->save();
+
+		return $book;
+	}
+
+	public function test_save()
+	{
+		$venue = new Venue(array('name' => 'Tito'));
+		$venue->save();
+	}
+
+	public function test_insert()
+	{
+		$author = new Author(array('name' => 'Blah Blah'));
+		$author->save();
+		$this->assert_not_null(Author::find($author->id));
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_insert_with_no_sequence_defined()
+	{
+		if (!$this->conn->supports_sequences())
+			throw new ActiveRecord\DatabaseException('');
+
+		AuthorWithoutSequence::create(array('name' => 'Bob!'));
+	}
+
+	public function test_insert_should_quote_keys()
+	{
+		$author = new Author(array('name' => 'Blah Blah'));
+		$author->save();
+		$this->assert_true(strpos($author->connection()->last_query,$author->connection()->quote_name('updated_at')) !== false);
+	}
+
+	public function test_save_auto_increment_id()
+	{
+		$venue = new Venue(array('name' => 'Bob'));
+		$venue->save();
+		$this->assert_true($venue->id > 0);
+	}
+
+	public function test_sequence_was_set()
+	{
+		if ($this->conn->supports_sequences())
+			$this->assert_equals($this->conn->get_sequence_name('authors','author_id'),Author::table()->sequence);
+		else
+			$this->assert_null(Author::table()->sequence);
+	}
+
+	public function test_sequence_was_explicitly_set()
+	{
+		if ($this->conn->supports_sequences())
+			$this->assert_equals(AuthorExplicitSequence::$sequence,AuthorExplicitSequence::table()->sequence);
+		else
+			$this->assert_null(Author::table()->sequence);
+	}
+
+	public function test_delete()
+	{
+		$author = Author::find(1);
+		$author->delete();
+
+		$this->assert_false(Author::exists(1));
+	}
+
+	public function test_delete_by_find_all()
+	{
+		$books = Book::all();
+
+		foreach ($books as $model)
+			$model->delete();
+
+		$res = Book::all();
+		$this->assert_equals(0,count($res));
+	}
+
+	public function test_update()
+	{
+		$book = Book::find(1);
+		$new_name = 'new name';
+		$book->name = $new_name;
+		$book->save();
+
+		$this->assert_same($new_name, $book->name);
+		$this->assert_same($new_name, $book->name, Book::find(1)->name);
+	}
+
+	public function test_update_should_quote_keys()
+	{
+		$book = Book::find(1);
+		$book->name = 'new name';
+		$book->save();
+		$this->assert_true(strpos($book->connection()->last_query,$book->connection()->quote_name('name')) !== false);
+	}
+
+	public function test_update_attributes()
+	{
+		$book = Book::find(1);
+		$new_name = 'How to lose friends and alienate people'; // jax i'm worried about you
+		$attrs = array('name' => $new_name);
+		$book->update_attributes($attrs);
+
+		$this->assert_same($new_name, $book->name);
+		$this->assert_same($new_name, $book->name, Book::find(1)->name);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\UndefinedPropertyException
+	 */
+	public function test_update_attributes_undefined_property()
+	{
+		$book = Book::find(1);
+		$book->update_attributes(array('name' => 'new name', 'invalid_attribute' => true , 'another_invalid_attribute' => 'blah'));
+	}
+
+	public function test_update_attribute()
+	{
+		$book = Book::find(1);
+		$new_name = 'some stupid self-help book';
+		$book->update_attribute('name', $new_name);
+
+		$this->assert_same($new_name, $book->name);
+		$this->assert_same($new_name, $book->name, Book::find(1)->name);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\UndefinedPropertyException
+	 */
+	public function test_update_attribute_undefined_property()
+	{
+		$book = Book::find(1);
+		$book->update_attribute('invalid_attribute', true);
+	}
+
+	public function test_save_null_value()
+	{
+		$book = Book::first();
+		$book->name = null;
+		$book->save();
+		$this->assert_same(null,Book::find($book->id)->name);
+	}
+
+	public function test_save_blank_value()
+	{
+		// oracle doesn't do blanks. probably an option to enable?
+		if ($this->conn instanceof ActiveRecord\OciAdapter)
+			return;
+
+		$book = Book::find(1);
+		$book->name = '';
+		$book->save();
+		$this->assert_same('',Book::find(1)->name);
+	}
+
+	public function test_dirty_attributes()
+	{
+		$book = $this->make_new_book_and(false);
+		$this->assert_equals(array('name','special'),array_keys($book->dirty_attributes()));
+	}
+
+	public function test_dirty_attributes_cleared_after_saving()
+	{
+		$book = $this->make_new_book_and();
+		$this->assert_true(strpos($book->table()->last_sql,'name') !== false);
+		$this->assert_true(strpos($book->table()->last_sql,'special') !== false);
+		$this->assert_equals(null,$book->dirty_attributes());
+	}
+
+	public function test_dirty_attributes_cleared_after_inserting()
+	{
+		$book = $this->make_new_book_and();
+		$this->assert_equals(null,$book->dirty_attributes());
+	}
+
+	public function test_no_dirty_attributes_but_still_insert_record()
+	{
+		$book = new Book;
+		$this->assert_equals(null,$book->dirty_attributes());
+		$book->save();
+		$this->assert_equals(null,$book->dirty_attributes());
+		$this->assert_not_null($book->id);
+	}
+
+	public function test_dirty_attributes_cleared_after_updating()
+	{
+		$book = Book::first();
+		$book->name = 'rivers cuomo';
+		$book->save();
+		$this->assert_equals(null,$book->dirty_attributes());
+	}
+
+	public function test_dirty_attributes_after_reloading()
+	{
+		$book = Book::first();
+		$book->name = 'rivers cuomo';
+		$book->reload();
+		$this->assert_equals(null,$book->dirty_attributes());
+	}
+
+	public function test_dirty_attributes_with_mass_assignment()
+	{
+		$book = Book::first();
+		$book->set_attributes(array('name' => 'rivers cuomo'));
+		$this->assert_equals(array('name'), array_keys($book->dirty_attributes()));
+	}
+
+	public function test_timestamps_set_before_save()
+	{
+		$author = new Author;
+		$author->save();
+		$this->assert_not_null($author->created_at, $author->updated_at);
+
+		$author->reload();
+		$this->assert_not_null($author->created_at, $author->updated_at);
+	}
+
+	public function test_timestamps_updated_at_only_set_before_update()
+	{
+		$author = new Author();
+		$author->save();
+		$created_at = $author->created_at;
+		$updated_at = $author->updated_at;
+		sleep(1);
+
+		$author->name = 'test';
+		$author->save();
+
+		$this->assert_not_null($author->updated_at);
+		$this->assert_same($created_at, $author->created_at);
+		$this->assert_not_equals($updated_at, $author->updated_at);
+	}
+
+	public function test_create()
+	{
+		$author = Author::create(array('name' => 'Blah Blah'));
+		$this->assert_not_null(Author::find($author->id));
+	}
+
+	public function test_create_should_set_created_at()
+	{
+		$author = Author::create(array('name' => 'Blah Blah'));
+		$this->assert_not_null($author->created_at);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_update_with_no_primary_key_defined()
+	{
+		Author::table()->pk = array();
+		$author = Author::first();
+		$author->name = 'blahhhhhhhhhh';
+		$author->save();
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_delete_with_no_primary_key_defined()
+	{
+		Author::table()->pk = array();
+		$author = author::first();
+		$author->delete();
+	}
+
+	public function test_inserting_with_explicit_pk()
+	{
+		$author = Author::create(array('author_id' => 9999, 'name' => 'blah'));
+		$this->assert_equals(9999,$author->author_id);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ReadOnlyException
+	 */
+	public function test_readonly()
+	{
+		$author = Author::first(array('readonly' => true));
+		$author->save();
+	}
+
+	public function test_modified_attributes_in_before_handlers_get_saved()
+	{
+		$author = DirtyAuthor::first();
+		$author->encrypted_password = 'coco';
+		$author->save();
+		$this->assert_equals('i saved',DirtyAuthor::find($author->id)->name);
+	}
+
+	public function test_is_dirty()
+	{
+		$author = Author::first();
+		$this->assert_equals(false,$author->is_dirty());
+
+		$author->name = 'coco';
+		$this->assert_equals(true,$author->is_dirty());
+	}
+
+	public function test_set_date_flags_dirty()
+	{
+		$author = Author::create(array('some_date' => new DateTime()));
+		$author = Author::find($author->id);
+		$author->some_date->setDate(2010,1,1);
+		$this->assert_has_keys('some_date', $author->dirty_attributes());
+	}
+
+	public function test_set_date_flags_dirty_with_php_datetime()
+	{
+		$author = Author::create(array('some_date' => new \DateTime()));
+		$author = Author::find($author->id);
+		$author->some_date->setDate(2010,1,1);
+		$this->assert_has_keys('some_date', $author->dirty_attributes());
+	}
+
+	public function test_delete_all_with_conditions_as_string()
+	{
+		$num_affected = Author::delete_all(array('conditions' => 'parent_author_id = 2'));
+		$this->assert_equals(2, $num_affected);
+	}
+
+	public function test_delete_all_with_conditions_as_hash()
+	{
+		$num_affected = Author::delete_all(array('conditions' => array('parent_author_id' => 2)));
+		$this->assert_equals(2, $num_affected);
+	}
+
+	public function test_delete_all_with_conditions_as_array()
+	{
+		$num_affected = Author::delete_all(array('conditions' => array('parent_author_id = ?', 2)));
+		$this->assert_equals(2, $num_affected);
+	}
+
+	public function test_delete_all_with_limit_and_order()
+	{
+		if (!$this->conn->accepts_limit_and_order_for_update_and_delete())
+			$this->mark_test_skipped('Only MySQL & Sqlite accept limit/order with UPDATE clause');
+
+		$num_affected = Author::delete_all(array('conditions' => array('parent_author_id = ?', 2), 'limit' => 1, 'order' => 'name asc'));
+		$this->assert_equals(1, $num_affected);
+		$this->assert_true(strpos(Author::table()->last_sql, 'ORDER BY name asc LIMIT 1') !== false);
+	}
+
+	public function test_update_all_with_set_as_string()
+	{
+		$num_affected = Author::update_all(array('set' => 'parent_author_id = 2'));
+		$this->assert_equals(2, $num_affected);
+		$this->assert_equals(4, Author::count_by_parent_author_id(2));
+	}
+
+	public function test_update_all_with_set_as_hash()
+	{
+		$num_affected = Author::update_all(array('set' => array('parent_author_id' => 2)));
+		$this->assert_equals(2, $num_affected);
+	}
+
+	/**
+	 * TODO: not implemented
+	public function test_update_all_with_set_as_array()
+	{
+		$num_affected = Author::update_all(array('set' => array('parent_author_id = ?', 2)));
+		$this->assert_equals(2, $num_affected);
+	}
+	 */
+
+	public function test_update_all_with_conditions_as_string()
+	{
+		$num_affected = Author::update_all(array('set' => 'parent_author_id = 2', 'conditions' => 'name = "Tito"'));
+		$this->assert_equals(1, $num_affected);
+	}
+
+	public function test_update_all_with_conditions_as_hash()
+	{
+		$num_affected = Author::update_all(array('set' => 'parent_author_id = 2', 'conditions' => array('name' => "Tito")));
+		$this->assert_equals(1, $num_affected);
+	}
+
+	public function test_update_all_with_conditions_as_array()
+	{
+		$num_affected = Author::update_all(array('set' => 'parent_author_id = 2', 'conditions' => array('name = ?', "Tito")));
+		$this->assert_equals(1, $num_affected);
+	}
+
+	public function test_update_all_with_limit_and_order()
+	{
+		if (!$this->conn->accepts_limit_and_order_for_update_and_delete())
+			$this->mark_test_skipped('Only MySQL & Sqlite accept limit/order with UPDATE clause');
+
+		$num_affected = Author::update_all(array('set' => 'parent_author_id = 2', 'limit' => 1, 'order' => 'name asc'));
+		$this->assert_equals(1, $num_affected);
+		$this->assert_true(strpos(Author::table()->last_sql, 'ORDER BY name asc LIMIT 1') !== false);
+	}
+};

+ 24 - 0
hhvm/php-activerecord/test/AllTests.php

@@ -0,0 +1,24 @@
+<?php
+
+foreach (glob('*Test.php') as $file)
+{
+	if ($file != 'AllValidationsTest.php')
+		require $file;
+}
+
+class AllTests
+{
+	public static function suite()
+	{
+		$suite = new PHPUnit_Framework_TestSuite('PHPUnit');
+
+		foreach (glob('*Test.php') as $file)
+		{
+			if ($file != 'AllValidationsTest.php')
+				$suite->addTestSuite(substr($file,0,-4));
+		}
+
+		return $suite;
+	}
+}
+?>

+ 21 - 0
hhvm/php-activerecord/test/AllValidationsTest.php

@@ -0,0 +1,21 @@
+<?php
+require_once 'helpers/config.php';
+
+foreach (glob('Validates*Test.php') as $file)
+	require $file;
+require "ValidationsTest.php";
+
+class AllValidationsTests extends DatabaseTest
+{
+	public static function suite()
+	{
+		$suite = new PHPUnit_Framework_TestSuite('PHPUnit');
+
+		foreach (glob('Validates*Test.php') as $file)
+			$suite->addTestSuite(substr($file,0,-4));
+		$suite->addTestSuite('ValidationsTest');
+
+		return $suite;
+	}
+};
+?>

+ 84 - 0
hhvm/php-activerecord/test/CacheTest.php

@@ -0,0 +1,84 @@
+<?php
+include 'helpers/config.php';
+
+use ActiveRecord\Cache;
+
+class CacheTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	public function set_up()
+	{
+		if (!extension_loaded('memcache'))
+		{
+			$this->markTestSkipped('The memcache extension is not available');
+			return;
+		}
+		
+		Cache::initialize('memcache://localhost');
+	}
+
+	public function tear_down()
+	{
+		Cache::flush();
+	}
+
+	private function cache_get()
+	{
+		return Cache::get("1337", function() { return "abcd"; });
+	}
+
+	public function test_initialize()
+	{
+		$this->assert_not_null(Cache::$adapter);
+	}
+
+	public function test_initialize_with_null()
+	{
+		Cache::initialize(null);
+		$this->assert_null(Cache::$adapter);
+	}
+
+	public function test_get_returns_the_value()
+	{
+		$this->assert_equals("abcd", $this->cache_get());
+	}
+
+	public function test_get_writes_to_the_cache()
+	{
+		$this->cache_get();
+		$this->assert_equals("abcd", Cache::$adapter->read("1337"));
+	}
+
+	public function test_get_does_not_execute_closure_on_cache_hit()
+	{
+		$this->cache_get();
+		Cache::get("1337", function() { throw new Exception("I better not execute!"); });
+	}
+
+	public function test_cache_adapter_returns_false_on_cache_miss()
+	{
+		$this->assert_same(false, Cache::$adapter->read("some-key"));
+	}
+
+	public function test_get_works_without_caching_enabled()
+	{
+		Cache::$adapter = null;
+		$this->assert_equals("abcd", $this->cache_get());
+	}
+
+	public function test_cache_expire()
+	{
+		Cache::$options['expire'] = 1;
+		$this->cache_get();
+		sleep(2);
+
+		$this->assert_same(false, Cache::$adapter->read("1337"));
+	}
+	
+	public function test_namespace_is_set_properly()
+	{
+	  Cache::$options['namespace'] = 'myapp';
+	  $this->cache_get();
+	  $this->assert_same("abcd", Cache::$adapter->read("myapp::1337"));
+	}
+}
+?>

+ 282 - 0
hhvm/php-activerecord/test/CallbackTest.php

@@ -0,0 +1,282 @@
+<?php
+include 'helpers/config.php';
+
+class CallBackTest extends DatabaseTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+
+		// ensure VenueCB model has been loaded
+		VenueCB::find(1);
+
+		$this->callback = new ActiveRecord\CallBack('VenueCB');
+	}
+
+	public function assert_has_callback($callback_name, $method_name=null)
+	{
+		if (!$method_name)
+			$method_name = $callback_name;
+
+		$this->assert_true(in_array($method_name,$this->callback->get_callbacks($callback_name)));
+	}
+
+	public function assert_implicit_save($first_method, $second_method)
+	{
+		$i_ran = array();
+		$this->callback->register($first_method,function($model) use (&$i_ran, $first_method) { $i_ran[] = $first_method; });
+		$this->callback->register($second_method,function($model) use (&$i_ran, $second_method) { $i_ran[] = $second_method; });
+		$this->callback->invoke(null,$second_method);
+		$this->assert_equals(array($first_method,$second_method),$i_ran);
+	}
+
+	public function test_generic_callback_was_auto_registered()
+	{
+		$this->assert_has_callback('after_construct');
+	}
+
+	public function test_register()
+	{
+		$this->callback->register('after_construct');
+		$this->assert_has_callback('after_construct');
+	}
+
+	public function test_register_non_generic()
+	{
+		$this->callback->register('after_construct','non_generic_after_construct');
+		$this->assert_has_callback('after_construct','non_generic_after_construct');
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_register_invalid_callback()
+	{
+		$this->callback->register('invalid_callback');
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_register_callback_with_undefined_method()
+	{
+		$this->callback->register('after_construct','do_not_define_me');
+	}
+
+	public function test_register_with_string_definition()
+	{
+		$this->callback->register('after_construct','after_construct');
+		$this->assert_has_callback('after_construct');
+	}
+
+	public function test_register_with_closure()
+	{
+		$this->callback->register('after_construct',function($mode) { });
+	}
+
+	public function test_register_with_null_definition()
+	{
+		$this->callback->register('after_construct',null);
+		$this->assert_has_callback('after_construct');
+	}
+
+	public function test_register_with_no_definition()
+	{
+		$this->callback->register('after_construct');
+		$this->assert_has_callback('after_construct');
+	}
+
+	public function test_register_appends_to_registry()
+	{
+		$this->callback->register('after_construct');
+		$this->callback->register('after_construct','non_generic_after_construct');
+		$this->assert_equals(array('after_construct','after_construct','non_generic_after_construct'),$this->callback->get_callbacks('after_construct'));
+	}
+
+	public function test_register_prepends_to_registry()
+	{
+		$this->callback->register('after_construct');
+		$this->callback->register('after_construct','non_generic_after_construct',array('prepend' => true));
+		$this->assert_equals(array('non_generic_after_construct','after_construct','after_construct'),$this->callback->get_callbacks('after_construct'));
+	}
+
+	public function test_registers_via_static_array_definition()
+	{
+		$this->assert_has_callback('after_destroy','after_destroy_one');
+		$this->assert_has_callback('after_destroy','after_destroy_two');
+	}
+
+	public function test_registers_via_static_string_definition()
+	{
+		$this->assert_has_callback('before_destroy','before_destroy_using_string');
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_register_via_static_with_invalid_definition()
+	{
+		$class_name = "Venues_" . md5(uniqid());
+		eval("class $class_name extends ActiveRecord\\Model { static \$table_name = 'venues'; static \$after_save = 'method_that_does_not_exist'; };");
+		new $class_name();
+		new ActiveRecord\CallBack($class_name);
+	}
+
+	public function test_can_register_same_multiple_times()
+	{
+		$this->callback->register('after_construct');
+		$this->callback->register('after_construct');
+		$this->assert_equals(array('after_construct','after_construct','after_construct'),$this->callback->get_callbacks('after_construct'));
+	}
+
+	public function test_register_closure_callback()
+	{
+		$closure = function($model) {};
+		$this->callback->register('after_save',$closure);
+		$this->assert_equals(array($closure),$this->callback->get_callbacks('after_save'));
+	}
+
+	public function test_get_callbacks_returns_array()
+	{
+		$this->callback->register('after_construct');
+		$this->assert_true(is_array($this->callback->get_callbacks('after_construct')));
+	}
+
+	public function test_get_callbacks_returns_null()
+	{
+		$this->assert_null($this->callback->get_callbacks('this_callback_name_should_never_exist'));
+	}
+
+	public function test_invoke_runs_all_callbacks()
+	{
+		$mock = $this->get_mock('VenueCB',array('after_destroy_one','after_destroy_two'));
+		$mock->expects($this->once())->method('after_destroy_one');
+		$mock->expects($this->once())->method('after_destroy_two');
+		$this->callback->invoke($mock,'after_destroy');
+	}
+
+	public function test_invoke_closure()
+	{
+		$i_ran = false;
+		$this->callback->register('after_validation',function($model) use (&$i_ran) { $i_ran = true; });
+		$this->callback->invoke(null,'after_validation');
+		$this->assert_true($i_ran);
+	}
+
+	public function test_invoke_implicitly_calls_save_first()
+	{
+		$this->assert_implicit_save('before_save','before_create');
+		$this->assert_implicit_save('before_save','before_update');
+		$this->assert_implicit_save('after_save','after_create');
+		$this->assert_implicit_save('after_save','after_update');
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_invoke_unregistered_callback()
+	{
+		$mock = $this->get_mock('VenueCB', array('columns'));
+		$this->callback->invoke($mock,'before_validation_on_create');
+	}
+
+	public function test_before_callbacks_pass_on_false_return_callback_returned_false()
+	{
+		$this->callback->register('before_validation',function($model) { return false; });
+		$this->assert_false($this->callback->invoke(null,'before_validation'));
+	}
+
+	public function test_before_callbacks_does_not_pass_on_false_for_after_callbacks()
+	{
+		$this->callback->register('after_validation',function($model) { return false; });
+		$this->assert_true($this->callback->invoke(null,'after_validation'));
+	}
+
+	public function test_gh_28_after_create_should_be_invoked_after_auto_incrementing_pk_is_set()
+	{
+		$that = $this;
+		VenueCB::$after_create = function($model) use ($that) { $that->assert_not_null($model->id); };
+		ActiveRecord\Table::clear_cache('VenueCB');
+		$venue = VenueCB::find(1);
+		$venue = new VenueCB($venue->attributes());
+		$venue->id = null;
+		$venue->name = 'alksdjfs';
+		$venue->save();
+	}
+
+	public function test_before_create_returned_false_halts_execution()
+	{
+		VenueCB::$before_create = array('before_create_halt_execution');
+		ActiveRecord\Table::clear_cache('VenueCB');
+		$table = ActiveRecord\Table::load('VenueCB');
+
+		$i_ran = false;
+		$i_should_have_ran = false;
+		$table->callback->register('before_save', function($model) use (&$i_should_have_ran) { $i_should_have_ran = true; });
+		$table->callback->register('before_create',function($model) use (&$i_ran) { $i_ran = true; });
+		$table->callback->register('after_create',function($model) use (&$i_ran) { $i_ran = true; });
+
+		$v = VenueCB::find(1);
+		$v->id = null;
+		VenueCB::create($v->attributes());
+
+		$this->assert_true($i_should_have_ran);
+		$this->assert_false($i_ran);
+		$this->assert_true(strpos(ActiveRecord\Table::load('VenueCB')->last_sql, 'INSERT') === false);
+	}
+
+	public function test_before_save_returned_false_halts_execution()
+	{
+		VenueCB::$before_update = array('before_update_halt_execution');
+		ActiveRecord\Table::clear_cache('VenueCB');
+		$table = ActiveRecord\Table::load('VenueCB');
+
+		$i_ran = false;
+		$i_should_have_ran = false;
+		$table->callback->register('before_save', function($model) use (&$i_should_have_ran) { $i_should_have_ran = true; });
+		$table->callback->register('before_update',function($model) use (&$i_ran) { $i_ran = true; });
+		$table->callback->register('after_save',function($model) use (&$i_ran) { $i_ran = true; });
+
+		$v = VenueCB::find(1);
+		$v->name .= 'test';
+		$ret = $v->save();
+
+		$this->assert_true($i_should_have_ran);
+		$this->assert_false($i_ran);
+		$this->assert_false($ret);
+		$this->assert_true(strpos(ActiveRecord\Table::load('VenueCB')->last_sql, 'UPDATE') === false);
+	}
+
+	public function test_before_destroy_returned_false_halts_execution()
+	{
+		VenueCB::$before_destroy = array('before_destroy_halt_execution');
+		ActiveRecord\Table::clear_cache('VenueCB');
+		$table = ActiveRecord\Table::load('VenueCB');
+
+		$i_ran = false;
+		$table->callback->register('before_destroy',function($model) use (&$i_ran) { $i_ran = true; });
+		$table->callback->register('after_destroy',function($model) use (&$i_ran) { $i_ran = true; });
+
+		$v = VenueCB::find(1);
+		$ret = $v->delete();
+
+		$this->assert_false($i_ran);
+		$this->assert_false($ret);
+		$this->assert_true(strpos(ActiveRecord\Table::load('VenueCB')->last_sql, 'DELETE') === false);
+	}
+
+	public function test_before_validation_returned_false_halts_execution()
+	{
+		VenueCB::$before_validation = array('before_validation_halt_execution');
+		ActiveRecord\Table::clear_cache('VenueCB');
+		$table = ActiveRecord\Table::load('VenueCB');
+
+		$v = VenueCB::find(1);
+		$v->name .= 'test';
+		$ret = $v->save();
+
+		$this->assert_false($ret);
+		$this->assert_true(strpos(ActiveRecord\Table::load('VenueCB')->last_sql, 'UPDATE') === false);
+	}
+};
+?>

+ 122 - 0
hhvm/php-activerecord/test/ColumnTest.php

@@ -0,0 +1,122 @@
+<?php
+include 'helpers/config.php';
+
+use ActiveRecord\Column;
+use ActiveRecord\DateTime;
+use ActiveRecord\DatabaseException;
+
+class ColumnTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	public function set_up()
+	{
+		$this->column = new Column();
+		try {
+			$this->conn = ActiveRecord\ConnectionManager::get_connection(ActiveRecord\Config::instance()->get_default_connection());
+		} catch (DatabaseException $e) {
+			$this->mark_test_skipped('failed to connect using default connection. '.$e->getMessage());
+		}
+	}
+
+	public function assert_mapped_type($type, $raw_type)
+	{
+		$this->column->raw_type = $raw_type;
+		$this->assert_equals($type,$this->column->map_raw_type());
+	}
+
+	public function assert_cast($type, $casted_value, $original_value)
+	{
+		$this->column->type = $type;
+		$value = $this->column->cast($original_value,$this->conn);
+
+		if ($original_value != null && ($type == Column::DATETIME || $type == Column::DATE))
+			$this->assert_true($value instanceof DateTime);
+		else
+			$this->assert_same($casted_value,$value);
+	}
+
+	public function test_map_raw_type_dates()
+	{
+		$this->assert_mapped_type(Column::DATETIME,'datetime');
+		$this->assert_mapped_type(Column::DATE,'date');
+	}
+
+	public function test_map_raw_type_integers()
+	{
+		$this->assert_mapped_type(Column::INTEGER,'integer');
+		$this->assert_mapped_type(Column::INTEGER,'int');
+		$this->assert_mapped_type(Column::INTEGER,'tinyint');
+		$this->assert_mapped_type(Column::INTEGER,'smallint');
+		$this->assert_mapped_type(Column::INTEGER,'mediumint');
+		$this->assert_mapped_type(Column::INTEGER,'bigint');
+	}
+
+	public function test_map_raw_type_decimals()
+	{
+		$this->assert_mapped_type(Column::DECIMAL,'float');
+		$this->assert_mapped_type(Column::DECIMAL,'double');
+		$this->assert_mapped_type(Column::DECIMAL,'numeric');
+		$this->assert_mapped_type(Column::DECIMAL,'dec');
+	}
+
+	public function test_map_raw_type_strings()
+	{
+		$this->assert_mapped_type(Column::STRING,'string');
+		$this->assert_mapped_type(Column::STRING,'varchar');
+		$this->assert_mapped_type(Column::STRING,'text');
+	}
+
+	public function test_map_raw_type_default_to_string()
+	{
+		$this->assert_mapped_type(Column::STRING,'bajdslfjasklfjlksfd');
+	}
+
+	public function test_map_raw_type_changes_integer_to_int()
+	{
+		$this->column->raw_type = 'integer';
+		$this->column->map_raw_type();
+		$this->assert_equals('int',$this->column->raw_type);
+	}
+
+	public function test_cast()
+	{
+		$datetime = new DateTime('2001-01-01');
+		$this->assert_cast(Column::INTEGER,1,'1');
+		$this->assert_cast(Column::INTEGER,1,'1.5');
+		$this->assert_cast(Column::DECIMAL,1.5,'1.5');
+		$this->assert_cast(Column::DATETIME,$datetime,'2001-01-01');
+		$this->assert_cast(Column::DATE,$datetime,'2001-01-01');
+		$this->assert_cast(Column::DATE,$datetime,$datetime);
+		$this->assert_cast(Column::STRING,'bubble tea','bubble tea');
+	}
+
+	public function test_cast_leave_null_alone()
+	{
+		$types = array(
+			Column::STRING,
+			Column::INTEGER,
+			Column::DECIMAL,
+			Column::DATETIME,
+			Column::DATE);
+
+		foreach ($types as $type) {
+			$this->assert_cast($type,null,null);
+		}
+	}
+
+	public function test_empty_and_null_date_strings_should_return_null()
+	{
+		$column = new Column();
+		$column->type = Column::DATE;
+		$this->assert_equals(null,$column->cast(null,$this->conn));
+		$this->assert_equals(null,$column->cast('',$this->conn));
+	}
+
+	public function test_empty_and_null_datetime_strings_should_return_null()
+	{
+		$column = new Column();
+		$column->type = Column::DATETIME;
+		$this->assert_equals(null,$column->cast(null,$this->conn));
+		$this->assert_equals(null,$column->cast('',$this->conn));
+	}
+}
+?>

+ 96 - 0
hhvm/php-activerecord/test/ConfigTest.php

@@ -0,0 +1,96 @@
+<?php
+include 'helpers/config.php';
+
+use ActiveRecord\Config;
+use ActiveRecord\ConfigException;
+
+class TestLogger
+{
+	private function log() {}
+}
+
+class ConfigTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	public function set_up()
+	{
+		$this->config = new Config();
+		$this->connections = array('development' => 'mysql://blah/development', 'test' => 'mysql://blah/test');
+		$this->config->set_connections($this->connections);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ConfigException
+	 */
+	public function test_set_connections_must_be_array()
+	{
+		$this->config->set_connections(null);
+	}
+
+	public function test_get_connections()
+	{
+		$this->assert_equals($this->connections,$this->config->get_connections());
+	}
+
+	public function test_get_connection()
+	{
+		$this->assert_equals($this->connections['development'],$this->config->get_connection('development'));
+	}
+
+	public function test_get_invalid_connection()
+	{
+		$this->assert_null($this->config->get_connection('whiskey tango foxtrot'));
+	}
+
+	public function test_get_default_connection_and_connection()
+	{
+		$this->config->set_default_connection('development');
+		$this->assert_equals('development',$this->config->get_default_connection());
+		$this->assert_equals($this->connections['development'],$this->config->get_default_connection_string());
+	}
+
+	public function test_get_default_connection_and_connection_string_defaults_to_development()
+	{
+		$this->assert_equals('development',$this->config->get_default_connection());
+		$this->assert_equals($this->connections['development'],$this->config->get_default_connection_string());
+	}
+
+	public function test_get_default_connection_string_when_connection_name_is_not_valid()
+	{
+		$this->config->set_default_connection('little mac');
+		$this->assert_null($this->config->get_default_connection_string());
+	}
+
+	public function test_default_connection_is_set_when_only_one_connection_is_present()
+	{
+		$this->config->set_connections(array('development' => $this->connections['development']));
+		$this->assert_equals('development',$this->config->get_default_connection());
+	}
+
+	public function test_set_connections_with_default()
+	{
+		$this->config->set_connections($this->connections,'test');
+		$this->assert_equals('test',$this->config->get_default_connection());
+	}
+
+	public function test_initialize_closure()
+	{
+		$test = $this;
+
+		Config::initialize(function($cfg) use ($test)
+		{
+			$test->assert_not_null($cfg);
+			$test->assert_equals('ActiveRecord\Config',get_class($cfg));
+		});
+	}
+
+	public function test_logger_object_must_implement_log_method()
+	{
+		try {
+			$this->config->set_logger(new TestLogger);
+			$this->fail();
+		} catch (ConfigException $e) {
+			$this->assert_equals($e->getMessage(), "Logger object must implement a public log method");
+		}
+	}
+}
+?>

+ 40 - 0
hhvm/php-activerecord/test/ConnectionManagerTest.php

@@ -0,0 +1,40 @@
+<?php
+include 'helpers/config.php';
+
+use ActiveRecord\Config;
+use ActiveRecord\ConnectionManager;
+
+class ConnectionManagerTest extends DatabaseTest
+{
+	public function test_get_connection_with_null_connection()
+	{
+		$this->assert_not_null(ConnectionManager::get_connection(null));
+		$this->assert_not_null(ConnectionManager::get_connection());
+	}
+    
+	public function test_get_connection()
+	{
+		$this->assert_not_null(ConnectionManager::get_connection('mysql'));
+	}
+
+	public function test_get_connection_uses_existing_object()
+	{
+		$a = ConnectionManager::get_connection('mysql');
+		$a->harro = 'harro there';
+
+		$this->assert_same($a,ConnectionManager::get_connection('mysql'));
+	}
+
+    public function test_gh_91_get_connection_with_null_connection_is_always_default()
+    {
+        $conn_one = ConnectionManager::get_connection('mysql');
+        $conn_two = ConnectionManager::get_connection();
+        $conn_three = ConnectionManager::get_connection('mysql');
+        $conn_four = ConnectionManager::get_connection();
+
+        $this->assert_same($conn_one, $conn_three);
+        $this->assert_same($conn_two, $conn_three);
+        $this->assert_same($conn_four, $conn_three);
+    }
+}
+?>

+ 81 - 0
hhvm/php-activerecord/test/ConnectionTest.php

@@ -0,0 +1,81 @@
+<?php
+use ActiveRecord\Connection;
+
+include 'helpers/config.php';
+
+// Only use this to test static methods in Connection that are not specific
+// to any database adapter.
+
+class ConnectionTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_connection_info_from_should_throw_exception_when_no_host()
+	{
+		ActiveRecord\Connection::parse_connection_url('mysql://user:pass@');
+	}
+
+	public function test_connection_info()
+	{
+		$info = ActiveRecord\Connection::parse_connection_url('mysql://user:[email protected]:3306/dbname');
+		$this->assert_equals('mysql',$info->protocol);
+		$this->assert_equals('user',$info->user);
+		$this->assert_equals('pass',$info->pass);
+		$this->assert_equals('127.0.0.1',$info->host);
+		$this->assert_equals(3306,$info->port);
+		$this->assert_equals('dbname',$info->db);
+	}
+	
+	public function test_gh_103_sqlite_connection_string_relative()
+	{
+		$info = ActiveRecord\Connection::parse_connection_url('sqlite://../some/path/to/file.db');
+		$this->assert_equals('../some/path/to/file.db', $info->host);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_gh_103_sqlite_connection_string_absolute()
+	{
+		$info = ActiveRecord\Connection::parse_connection_url('sqlite:///some/path/to/file.db');
+	}
+
+	public function test_gh_103_sqlite_connection_string_unix()
+	{
+		$info = ActiveRecord\Connection::parse_connection_url('sqlite://unix(/some/path/to/file.db)');
+		$this->assert_equals('/some/path/to/file.db', $info->host);
+       	
+		$info = ActiveRecord\Connection::parse_connection_url('sqlite://unix(/some/path/to/file.db)/');
+		$this->assert_equals('/some/path/to/file.db', $info->host);
+    	
+		$info = ActiveRecord\Connection::parse_connection_url('sqlite://unix(/some/path/to/file.db)/dummy');
+		$this->assert_equals('/some/path/to/file.db', $info->host);
+	}
+
+	public function test_gh_103_sqlite_connection_string_windows()
+	{
+		$info = ActiveRecord\Connection::parse_connection_url('sqlite://windows(c%3A/some/path/to/file.db)');
+		$this->assert_equals('c:/some/path/to/file.db', $info->host);
+	}
+
+	public function test_parse_connection_url_with_unix_sockets()
+	{
+		$info = ActiveRecord\Connection::parse_connection_url('mysql://user:password@unix(/tmp/mysql.sock)/database');
+		$this->assert_equals('/tmp/mysql.sock',$info->host);
+	}
+
+	public function test_parse_connection_url_with_decode_option()
+	{
+		$info = ActiveRecord\Connection::parse_connection_url('mysql://h%20az:h%[email protected]/test?decode=true');
+		$this->assert_equals('h az',$info->user);
+		$this->assert_equals('h@i',$info->pass);
+	}
+
+	public function test_encoding()
+	{
+		$info = ActiveRecord\Connection::parse_connection_url('mysql://test:[email protected]/test?charset=utf8');
+		$this->assert_equals('utf8', $info->charset);
+	}
+}
+?>

+ 19 - 0
hhvm/php-activerecord/test/DateFormatTest.php

@@ -0,0 +1,19 @@
+<?php
+include 'helpers/config.php';
+
+class DateFormatTest extends DatabaseTest
+{
+
+	public function test_datefield_gets_converted_to_ar_datetime()
+	{
+		//make sure first author has a date
+		$author = Author::first();
+		$author->some_date = new DateTime();
+		$author->save();
+		
+		$author = Author::first();
+		$this->assert_is_a("ActiveRecord\\DateTime",$author->some_date);
+	}
+
+};
+?>

+ 128 - 0
hhvm/php-activerecord/test/DateTimeTest.php

@@ -0,0 +1,128 @@
+<?php
+include 'helpers/config.php';
+use ActiveRecord\DateTime as DateTime;
+use ActiveRecord\DatabaseException;
+
+class DateTimeTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	public function set_up()
+	{
+		$this->date = new DateTime();
+		$this->original_format = DateTime::$DEFAULT_FORMAT;
+	}
+
+	public function tear_down()
+	{
+		DateTime::$DEFAULT_FORMAT = $this->original_format;
+	}
+
+	private function assert_dirtifies($method /*, method params, ...*/)
+	{
+		try {
+			$model = new Author();
+		} catch (DatabaseException $e) {
+			$this->mark_test_skipped('failed to connect. '.$e->getMessage());
+		}
+		$datetime = new DateTime();
+		$datetime->attribute_of($model,'some_date');
+
+		$args = func_get_args();
+		array_shift($args);
+
+		call_user_func_array(array($datetime,$method),$args);
+		$this->assert_has_keys('some_date', $model->dirty_attributes());
+	}
+
+	public function test_should_flag_the_attribute_dirty()
+	{
+		$this->assert_dirtifies('setDate',2001,1,1);
+		$this->assert_dirtifies('setISODate',2001,1);
+		$this->assert_dirtifies('setTime',1,1);
+		$this->assert_dirtifies('setTimestamp',1);
+	}
+
+	public function test_set_iso_date()
+	{
+		$a = new \DateTime();
+		$a->setISODate(2001,1);
+
+		$b = new DateTime();
+		$b->setISODate(2001,1);
+
+		$this->assert_datetime_equals($a,$b);
+	}
+
+	public function test_set_time()
+	{
+		$a = new \DateTime();
+		$a->setTime(1,1);
+
+		$b = new DateTime();
+		$b->setTime(1,1);
+
+		$this->assert_datetime_equals($a,$b);
+	}
+
+	public function test_get_format_with_friendly()
+	{
+		$this->assert_equals('Y-m-d H:i:s', DateTime::get_format('db'));
+	}
+
+	public function test_get_format_with_format()
+	{
+		$this->assert_equals('Y-m-d', DateTime::get_format('Y-m-d'));
+	}
+
+	public function test_get_format_with_null()
+	{
+		$this->assert_equals(\DateTime::RFC2822, DateTime::get_format());
+	}
+
+	public function test_format()
+	{
+		$this->assert_true(is_string($this->date->format()));
+		$this->assert_true(is_string($this->date->format('Y-m-d')));
+	}
+
+	public function test_format_by_friendly_name()
+	{
+		$d = date(DateTime::get_format('db'));
+		$this->assert_equals($d, $this->date->format('db'));
+	}
+
+	public function test_format_by_custom_format()
+	{
+		$format = 'Y/m/d';
+		$this->assert_equals(date($format), $this->date->format($format));
+	}
+
+	public function test_format_uses_default()
+	{
+		$d = date(DateTime::$FORMATS[DateTime::$DEFAULT_FORMAT]);
+		$this->assert_equals($d, $this->date->format());
+	}
+
+	public function test_all_formats()
+	{
+		foreach (DateTime::$FORMATS as $name => $format)
+			$this->assert_equals(date($format), $this->date->format($name));
+	}
+
+	public function test_change_default_format_to_format_string()
+	{
+		DateTime::$DEFAULT_FORMAT = 'H:i:s';
+		$this->assert_equals(date(DateTime::$DEFAULT_FORMAT), $this->date->format());
+	}
+
+	public function test_change_default_format_to_friently()
+	{
+		DateTime::$DEFAULT_FORMAT = 'short';
+		$this->assert_equals(date(DateTime::$FORMATS['short']), $this->date->format());
+	}
+
+	public function test_to_string()
+	{
+		$this->assert_equals(date(DateTime::get_format()), "" . $this->date);
+	}
+}
+?>

+ 202 - 0
hhvm/php-activerecord/test/ExpressionsTest.php

@@ -0,0 +1,202 @@
+<?php
+include 'helpers/config.php';
+require '../lib/Expressions.php';
+
+use ActiveRecord\Expressions;
+use ActiveRecord\ConnectionManager;
+use ActiveRecord\DatabaseException;
+
+class ExpressionsTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	public function test_values()
+	{
+		$c = new Expressions(null,'a=? and b=?',1,2);
+		$this->assert_equals(array(1,2), $c->values());
+	}
+
+	public function test_one_variable()
+	{
+		$c = new Expressions(null,'name=?','Tito');
+		$this->assert_equals('name=?',$c->to_s());
+		$this->assert_equals(array('Tito'),$c->values());
+	}
+
+	public function test_array_variable()
+	{
+		$c = new Expressions(null,'name IN(?) and id=?',array('Tito','George'),1);
+		$this->assert_equals(array(array('Tito','George'),1),$c->values());
+	}
+
+	public function test_multiple_variables()
+	{
+		$c = new Expressions(null,'name=? and book=?','Tito','Sharks');
+		$this->assert_equals('name=? and book=?',$c->to_s());
+		$this->assert_equals(array('Tito','Sharks'),$c->values());
+	}
+
+	public function test_to_string()
+	{
+		$c = new Expressions(null,'name=? and book=?','Tito','Sharks');
+		$this->assert_equals('name=? and book=?',$c->to_s());
+	}
+
+	public function test_to_string_with_array_variable()
+	{
+		$c = new Expressions(null,'name IN(?) and id=?',array('Tito','George'),1);
+		$this->assert_equals('name IN(?,?) and id=?',$c->to_s());
+	}
+
+	public function test_to_string_with_null_options()
+	{
+		$c = new Expressions(null,'name=? and book=?','Tito','Sharks');
+		$x = null;
+		$this->assert_equals('name=? and book=?',$c->to_s(false,$x));
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ExpressionsException
+	 */
+	public function test_insufficient_variables()
+	{
+		$c = new Expressions(null,'name=? and id=?','Tito');
+		$c->to_s();
+	}
+
+	public function test_no_values()
+	{
+		$c = new Expressions(null,"name='Tito'");
+		$this->assert_equals("name='Tito'",$c->to_s());
+		$this->assert_equals(0,count($c->values()));
+	}
+
+	public function test_null_variable()
+	{
+		$a = new Expressions(null,'name=?',null);
+		$this->assert_equals('name=?',$a->to_s());
+		$this->assert_equals(array(null),$a->values());
+	}
+
+	public function test_zero_variable()
+	{
+		$a = new Expressions(null,'name=?',0);
+		$this->assert_equals('name=?',$a->to_s());
+		$this->assert_equals(array(0),$a->values());
+	}
+
+	public function test_ignore_invalid_parameter_marker()
+	{
+		$a = new Expressions(null,"question='Do you love backslashes?' and id in(?)",array(1,2));
+		$this->assert_equals("question='Do you love backslashes?' and id in(?,?)",$a->to_s());
+	}
+
+	public function test_ignore_parameter_marker_with_escaped_quote()
+	{
+		$a = new Expressions(null,"question='Do you love''s backslashes?' and id in(?)",array(1,2));
+		$this->assert_equals("question='Do you love''s backslashes?' and id in(?,?)",$a->to_s());
+	}
+
+	public function test_ignore_parameter_marker_with_backspace_escaped_quote()
+	{
+		$a = new Expressions(null,"question='Do you love\\'s backslashes?' and id in(?)",array(1,2));
+		$this->assert_equals("question='Do you love\\'s backslashes?' and id in(?,?)",$a->to_s());
+	}
+
+	public function test_substitute()
+	{
+		$a = new Expressions(null,'name=? and id=?','Tito',1);
+		$this->assert_equals("name='Tito' and id=1",$a->to_s(true));
+	}
+
+	public function test_substitute_quotes_scalars_but_not_others()
+	{
+		$a = new Expressions(null,'id in(?)',array(1,'2',3.5));
+		$this->assert_equals("id in(1,'2',3.5)",$a->to_s(true));
+	}
+
+	public function test_substitute_where_value_has_question_mark()
+	{
+		$a = new Expressions(null,'name=? and id=?','??????',1);
+		$this->assert_equals("name='??????' and id=1",$a->to_s(true));
+	}
+
+	public function test_substitute_array_value()
+	{
+		$a = new Expressions(null,'id in(?)',array(1,2));
+		$this->assert_equals("id in(1,2)",$a->to_s(true));
+	}
+
+	public function test_substitute_escapes_quotes()
+	{
+		$a = new Expressions(null,'name=? or name in(?)',"Tito's Guild",array(1,"Tito's Guild"));
+		$this->assert_equals("name='Tito''s Guild' or name in(1,'Tito''s Guild')",$a->to_s(true));
+	}
+
+	public function test_substitute_escape_quotes_with_connections_escape_method()
+	{
+		try {
+			$conn = ConnectionManager::get_connection();
+		} catch (DatabaseException $e) {
+			$this->mark_test_skipped('failed to connect. '.$e->getMessage());
+		}
+		$a = new Expressions(null,'name=?',"Tito's Guild");
+		$a->set_connection($conn);
+		$escaped = $conn->escape("Tito's Guild");
+		$this->assert_equals("name=$escaped",$a->to_s(true));
+	}
+
+	public function test_bind()
+	{
+		$a = new Expressions(null,'name=? and id=?','Tito');
+		$a->bind(2,1);
+		$this->assert_equals(array('Tito',1),$a->values());
+	}
+
+	public function test_bind_overwrite_existing()
+	{
+		$a = new Expressions(null,'name=? and id=?','Tito',1);
+		$a->bind(2,99);
+		$this->assert_equals(array('Tito',99),$a->values());
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ExpressionsException
+	 */
+	public function test_bind_invalid_parameter_number()
+	{
+		$a = new Expressions(null,'name=?');
+		$a->bind(0,99);
+	}
+
+	public function test_subsitute_using_alternate_values()
+	{
+		$a = new Expressions(null,'name=?','Tito');
+		$this->assert_equals("name='Tito'",$a->to_s(true));
+		$x = array('values' => array('Hocus'));
+		$this->assert_equals("name='Hocus'",$a->to_s(true,$x));
+	}
+
+	public function test_null_value()
+	{
+		$a = new Expressions(null,'name=?',null);
+		$this->assert_equals('name=NULL',$a->to_s(true));
+	}
+
+	public function test_hash_with_default_glue()
+	{
+		$a = new Expressions(null,array('id' => 1, 'name' => 'Tito'));
+		$this->assert_equals('id=? AND name=?',$a->to_s());
+	}
+
+	public function test_hash_with_glue()
+	{
+		$a = new Expressions(null,array('id' => 1, 'name' => 'Tito'),', ');
+		$this->assert_equals('id=?, name=?',$a->to_s());
+	}
+
+	public function test_hash_with_array()
+	{
+		$a = new Expressions(null,array('id' => 1, 'name' => array('Tito','Mexican')));
+		$this->assert_equals('id=? AND name IN(?,?)',$a->to_s());
+	}
+}
+?>

+ 61 - 0
hhvm/php-activerecord/test/HasManyThroughTest.php

@@ -0,0 +1,61 @@
+<?php
+include 'helpers/config.php';
+include 'helpers/foo.php';
+
+use foo\bar\biz\User;
+use foo\bar\biz\Newsletter;
+
+class HasManyThroughTest extends DatabaseTest
+{
+	public function test_gh101_has_many_through()
+	{
+		$user = User::find(1);
+		$newsletter = Newsletter::find(1);
+
+		$this->assert_equals($newsletter->id, $user->newsletters[0]->id);
+		$this->assert_equals(
+			'foo\bar\biz\Newsletter',
+			get_class($user->newsletters[0])
+		);
+		$this->assert_equals($user->id, $newsletter->users[0]->id);
+		$this->assert_equals(
+			'foo\bar\biz\User',
+			get_class($newsletter->users[0])
+		);
+	}
+
+	public function test_gh101_has_many_through_include()
+	{
+		$user = User::find(1, array(
+			'include' => array(
+				'user_newsletters'
+			)
+		));
+
+		$this->assert_equals(1, $user->id);
+		$this->assert_equals(1, $user->user_newsletters[0]->id);
+	}
+
+	public function test_gh107_has_many_through_include_eager()
+	{
+		$venue = Venue::find(1, array('include' => array('events')));
+		$this->assert_equals(1, $venue->events[0]->id);
+
+		$venue = Venue::find(1, array('include' => array('hosts')));
+		$this->assert_equals(1, $venue->hosts[0]->id);
+	}
+
+	public function test_gh107_has_many_though_include_eager_with_namespace()
+	{
+		$user = User::find(1, array(
+			'include' => array(
+				'newsletters'
+			)
+		));
+
+		$this->assert_equals(1, $user->id);
+		$this->assert_equals(1, $user->newsletters[0]->id);
+	}
+}
+# vim: noet ts=4 nobinary
+?>

+ 29 - 0
hhvm/php-activerecord/test/InflectorTest.php

@@ -0,0 +1,29 @@
+<?php
+include 'helpers/config.php';
+require_once __DIR__ . '/../lib/Inflector.php';
+
+class InflectorTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	public function set_up()
+	{
+		$this->inflector = ActiveRecord\Inflector::instance();
+	}
+
+	public function test_underscorify()
+	{
+		$this->assert_equals('rm__name__bob',$this->inflector->variablize('rm--name  bob'));
+		$this->assert_equals('One_Two_Three',$this->inflector->underscorify('OneTwoThree'));
+	}
+
+	public function test_tableize()
+	{
+		$this->assert_equals('angry_people',$this->inflector->tableize('AngryPerson'));
+		$this->assert_equals('my_sqls',$this->inflector->tableize('MySQL'));
+	}
+
+	public function test_keyify()
+	{
+		$this->assert_equals('building_type_id', $this->inflector->keyify('BuildingType'));
+	}
+};
+?>

+ 98 - 0
hhvm/php-activerecord/test/ModelCallbackTest.php

@@ -0,0 +1,98 @@
+<?php
+include 'helpers/config.php';
+
+class ModelCallbackTest extends DatabaseTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+
+		$this->venue = new Venue();
+		$this->callback = Venue::table()->callback;
+	}
+
+	public function register_and_invoke_callbacks($callbacks, $return, $closure)
+	{
+		if (!is_array($callbacks))
+			$callbacks = array($callbacks);
+
+		$fired = array();
+
+		foreach ($callbacks as $name)
+			$this->callback->register($name,function($model) use (&$fired, $name, $return) { $fired[] = $name; return $return; });
+
+		$closure($this->venue);
+		return array_intersect($callbacks,$fired);
+	}
+
+	public function assert_fires($callbacks, $closure)
+	{
+		$executed = $this->register_and_invoke_callbacks($callbacks,true,$closure);
+		$this->assert_equals(count($callbacks),count($executed));
+	}
+
+	public function assert_does_not_fire($callbacks, $closure)
+	{
+		$executed = $this->register_and_invoke_callbacks($callbacks,true,$closure);
+		$this->assert_equals(0,count($executed));
+	}
+
+	public function assert_fires_returns_false($callbacks, $only_fire, $closure)
+	{
+		if (!is_array($only_fire))
+			$only_fire = array($only_fire);
+
+		$executed = $this->register_and_invoke_callbacks($callbacks,false,$closure);
+		sort($only_fire);
+		$intersect = array_intersect($only_fire,$executed);
+		sort($intersect);
+		$this->assert_equals($only_fire,$intersect);
+	}
+
+	public function test_after_construct_fires_by_default()
+	{
+		$this->assert_fires('after_construct',function($model) { new Venue(); });
+	}
+
+	public function test_fire_validation_callbacks_on_insert()
+	{
+		$this->assert_fires(array('before_validation','after_validation','before_validation_on_create','after_validation_on_create'),
+			function($model) { $model = new Venue(); $model->save(); });
+	}
+
+	public function test_fire_validation_callbacks_on_update()
+	{
+		$this->assert_fires(array('before_validation','after_validation','before_validation_on_update','after_validation_on_update'),
+			function($model) { $model = Venue::first(); $model->save(); });
+	}
+
+	public function test_validation_call_backs_not_fired_due_to_bypassing_validations()
+	{
+		$this->assert_does_not_fire('before_validation',function($model) { $model->save(false); });
+	}
+
+	public function test_before_validation_returning_false_cancels_callbacks()
+	{
+		$this->assert_fires_returns_false(array('before_validation','after_validation'),'before_validation',
+			function($model) { $model->save(); });
+	}
+
+	public function test_fires_before_save_and_before_update_when_updating()
+	{
+		$this->assert_fires(array('before_save','before_update'),
+			function($model) { $model = Venue::first(); $model->name = "something new"; $model->save(); });
+	}
+
+	public function test_before_save_returning_false_cancels_callbacks()
+	{
+		$this->assert_fires_returns_false(array('before_save','before_create'),'before_save',
+			function($model) { $model = new Venue(); $model->save(); });
+	}
+
+	public function test_destroy()
+	{
+		$this->assert_fires(array('before_destroy','after_destroy'),
+			function($model) { $model->delete(); });
+	}
+}
+?>

+ 38 - 0
hhvm/php-activerecord/test/MysqlAdapterTest.php

@@ -0,0 +1,38 @@
+<?php
+use ActiveRecord\Column;
+
+include 'helpers/config.php';
+require_once __DIR__ . '/../lib/adapters/MysqlAdapter.php';
+
+class MysqlAdapterTest extends AdapterTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up('mysql');
+	}
+
+	public function test_enum()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_equals('enum',$author_columns['some_enum']->raw_type);
+		$this->assert_equals(Column::STRING,$author_columns['some_enum']->type);
+		$this->assert_same(null,$author_columns['some_enum']->length);
+	}
+
+	public function test_set_charset()
+	{
+		$connection_string = ActiveRecord\Config::instance()->get_connection($this->connection_name);
+		$conn = ActiveRecord\Connection::instance($connection_string . '?charset=utf8');
+		$this->assert_equals('SET NAMES ?',$conn->last_query);
+	}
+
+	public function test_limit_with_null_offset_does_not_contain_offset()
+	{
+		$ret = array();
+		$sql = 'SELECT * FROM authors ORDER BY name ASC';
+		$this->conn->query_and_fetch($this->conn->limit($sql,null,1),function($row) use (&$ret) { $ret[] = $row; });
+
+		$this->assert_true(strpos($this->conn->last_query, 'LIMIT 1') !== false);
+	}
+}
+?>

+ 47 - 0
hhvm/php-activerecord/test/OciAdapterTest.php

@@ -0,0 +1,47 @@
+<?php
+include 'helpers/config.php';
+require_once __DIR__ . '/../lib/adapters/OciAdapter.php';
+
+class OciAdapterTest extends AdapterTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up('oci');
+	}
+
+	public function test_get_sequence_name()
+	{
+		$this->assert_equals('authors_seq',$this->conn->get_sequence_name('authors','author_id'));
+	}
+
+	public function test_columns_text()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_equals('varchar2',$author_columns['some_text']->raw_type);
+		$this->assert_equals(100,$author_columns['some_text']->length);
+	}
+
+	public function test_datetime_to_string()
+	{
+		$this->assert_equals('01-Jan-2009 01:01:01 AM',$this->conn->datetime_to_string(date_create('2009-01-01 01:01:01 EST')));
+	}
+
+	public function test_date_to_string()
+	{
+		$this->assert_equals('01-Jan-2009',$this->conn->date_to_string(date_create('2009-01-01 01:01:01 EST')));
+	}
+
+	public function test_insert_id() {}
+	public function test_insert_id_with_params() {}
+	public function test_insert_id_should_return_explicitly_inserted_id() {}
+	public function test_columns_time() {}
+	public function test_columns_sequence() {}
+
+	public function test_set_charset()
+	{
+		$connection_string = ActiveRecord\Config::instance()->get_connection($this->connection_name);
+		$conn = ActiveRecord\Connection::instance($connection_string . '?charset=utf8');
+		$this->assert_equals(';charset=utf8', $conn->dsn_params);
+	}
+}
+?>

+ 45 - 0
hhvm/php-activerecord/test/PgsqlAdapterTest.php

@@ -0,0 +1,45 @@
+<?php
+use ActiveRecord\Column;
+
+include 'helpers/config.php';
+require_once __DIR__ . '/../lib/adapters/PgsqlAdapter.php';
+
+class PgsqlAdapterTest extends AdapterTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up('pgsql');
+	}
+
+	public function test_insert_id()
+	{
+		$this->conn->query("INSERT INTO authors(author_id,name) VALUES(nextval('authors_author_id_seq'),'name')");
+		$this->assert_true($this->conn->insert_id('authors_author_id_seq') > 0);
+	}
+
+	public function test_insert_id_with_params()
+	{
+		$x = array('name');
+		$this->conn->query("INSERT INTO authors(author_id,name) VALUES(nextval('authors_author_id_seq'),?)",$x);
+		$this->assert_true($this->conn->insert_id('authors_author_id_seq') > 0);
+	}
+
+	public function test_insert_id_should_return_explicitly_inserted_id()
+	{
+		$this->conn->query('INSERT INTO authors(author_id,name) VALUES(99,\'name\')');
+		$this->assert_true($this->conn->insert_id('authors_author_id_seq') > 0);
+	}
+
+	public function test_set_charset()
+	{
+		$connection_string = ActiveRecord\Config::instance()->get_connection($this->connection_name);
+		$conn = ActiveRecord\Connection::instance($connection_string . '?charset=utf8');
+		$this->assert_equals("SET NAMES 'utf8'",$conn->last_query);
+	}
+
+	public function test_gh96_columns_not_duplicated_by_index()
+	{
+		$this->assert_equals(3,$this->conn->query_column_info("user_newsletters")->rowCount());
+	}
+}
+?>

+ 702 - 0
hhvm/php-activerecord/test/RelationshipTest.php

@@ -0,0 +1,702 @@
+<?php
+include 'helpers/config.php';
+
+class NotModel {};
+
+class AuthorWithNonModelRelationship extends ActiveRecord\Model
+{
+	static $pk = 'id';
+	static $table_name = 'authors';
+	static $has_many = array(array('books', 'class_name' => 'NotModel'));
+}
+
+class RelationshipTest extends DatabaseTest
+{
+	protected $relationship_name;
+	protected $relationship_names = array('has_many', 'belongs_to', 'has_one');
+
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+
+		Event::$belongs_to = array(array('venue'), array('host'));
+		Venue::$has_many = array(array('events', 'order' => 'id asc'),array('hosts', 'through' => 'events', 'order' => 'hosts.id asc'));
+		Venue::$has_one = array();
+		Employee::$has_one = array(array('position'));
+		Host::$has_many = array(array('events', 'order' => 'id asc'));
+
+		foreach ($this->relationship_names as $name)
+		{
+			if (preg_match("/$name/", $this->getName(), $match))
+				$this->relationship_name = $match[0];
+		}
+	}
+
+	protected function get_relationship($type=null)
+	{
+		if (!$type)
+			$type = $this->relationship_name;
+
+		switch ($type)
+		{
+			case 'belongs_to';
+				$ret = Event::find(5);
+				break;
+
+			case 'has_one';
+				$ret = Employee::find(1);
+				break;
+
+			case 'has_many';
+				$ret = Venue::find(2);
+				break;
+		}
+
+		return $ret;
+	}
+
+	protected function assert_default_belongs_to($event, $association_name='venue')
+	{
+		$this->assert_true($event->$association_name instanceof Venue);
+		$this->assert_equals(5,$event->id);
+		$this->assert_equals('West Chester',$event->$association_name->city);
+		$this->assert_equals(6,$event->$association_name->id);
+	}
+
+	protected function assert_default_has_many($venue, $association_name='events')
+	{
+		$this->assert_equals(2,$venue->id);
+		$this->assert_true(count($venue->$association_name) > 1);
+		$this->assert_equals('Yeah Yeah Yeahs',$venue->{$association_name}[0]->title);
+	}
+
+	protected function assert_default_has_one($employee, $association_name='position')
+	{
+		$this->assert_true($employee->$association_name instanceof Position);
+		$this->assert_equals('physicist',$employee->$association_name->title);
+		$this->assert_not_null($employee->id, $employee->$association_name->title);
+	}
+
+	public function test_has_many_basic()
+	{
+		$this->assert_default_has_many($this->get_relationship());
+	}
+
+	/**
+	 * @expectedException ActiveRecord\RelationshipException
+	 */
+	public function test_joins_on_model_via_undeclared_association()
+	{
+		$x = JoinBook::first(array('joins' => array('undeclared')));
+	}
+
+	public function test_joins_only_loads_given_model_attributes()
+	{
+		$x = Event::first(array('joins' => array('venue')));
+		$this->assert_sql_has('SELECT events.*',Event::table()->last_sql);
+		$this->assert_false(array_key_exists('city', $x->attributes()));
+	}
+
+	public function test_joins_combined_with_select_loads_all_attributes()
+	{
+		$x = Event::first(array('select' => 'events.*, venues.city as venue_city', 'joins' => array('venue')));
+		$this->assert_sql_has('SELECT events.*, venues.city as venue_city',Event::table()->last_sql);
+		$this->assert_true(array_key_exists('venue_city', $x->attributes()));
+	}
+
+	public function test_belongs_to_basic()
+	{
+		$this->assert_default_belongs_to($this->get_relationship());
+	}
+
+	public function test_belongs_to_returns_null_when_no_record()
+	{
+		$event = Event::find(6);
+		$this->assert_null($event->venue);
+	}
+
+	public function test_belongs_to_with_explicit_class_name()
+	{
+		Event::$belongs_to = array(array('explicit_class_name', 'class_name' => 'Venue'));
+		$this->assert_default_belongs_to($this->get_relationship(), 'explicit_class_name');
+	}
+
+	public function test_belongs_to_with_explicit_foreign_key()
+	{
+		$old = Book::$belongs_to;
+		Book::$belongs_to = array(array('explicit_author', 'class_name' => 'Author', 'foreign_key' => 'secondary_author_id'));
+
+		$book = Book::find(1);
+		$this->assert_equals(2, $book->secondary_author_id);
+		$this->assert_equals($book->secondary_author_id, $book->explicit_author->author_id);
+
+		Book::$belongs_to = $old;
+	}
+
+	public function test_belongs_to_with_select()
+	{
+		Event::$belongs_to[0]['select'] = 'id, city';
+		$event = $this->get_relationship();
+		$this->assert_default_belongs_to($event);
+
+		try {
+			$event->venue->name;
+			$this->fail('expected Exception ActiveRecord\UndefinedPropertyException');
+		} catch (ActiveRecord\UndefinedPropertyException $e) {
+			$this->assert_true(strpos($e->getMessage(), 'name') !== false);
+		}
+	}
+
+	public function test_belongs_to_with_readonly()
+	{
+		Event::$belongs_to[0]['readonly'] = true;
+		$event = $this->get_relationship();
+		$this->assert_default_belongs_to($event);
+
+		try {
+			$event->venue->save();
+			$this->fail('expected exception ActiveRecord\ReadonlyException');
+		} catch (ActiveRecord\ReadonlyException $e) {
+		}
+
+		$event->venue->name = 'new name';
+		$this->assert_equals($event->venue->name, 'new name');
+	}
+
+	public function test_belongs_to_with_plural_attribute_name()
+	{
+		Event::$belongs_to = array(array('venues', 'class_name' => 'Venue'));
+		$this->assert_default_belongs_to($this->get_relationship(), 'venues');
+	}
+
+	public function test_belongs_to_with_conditions_and_non_qualifying_record()
+	{
+		Event::$belongs_to[0]['conditions'] = "state = 'NY'";
+		$event = $this->get_relationship();
+		$this->assert_equals(5,$event->id);
+		$this->assert_null($event->venue);
+	}
+
+	public function test_belongs_to_with_conditions_and_qualifying_record()
+	{
+		Event::$belongs_to[0]['conditions'] = "state = 'PA'";
+		$this->assert_default_belongs_to($this->get_relationship());
+	}
+
+	public function test_belongs_to_build_association()
+	{
+		$event = $this->get_relationship();
+		$values = array('city' => 'Richmond', 'state' => 'VA');
+		$venue = $event->build_venue($values);
+		$this->assert_equals($values, array_intersect_key($values, $venue->attributes()));
+	}
+
+	public function test_has_many_build_association()
+	{
+		$author = Author::first();
+		$this->assert_equals($author->id, $author->build_books()->author_id);
+		$this->assert_equals($author->id, $author->build_book()->author_id);
+	}
+
+	public function test_belongs_to_create_association()
+	{
+		$event = $this->get_relationship();
+		$values = array('city' => 'Richmond', 'state' => 'VA', 'name' => 'Club 54', 'address' => '123 street');
+		$venue = $event->create_venue($values);
+		$this->assert_not_null($venue->id);
+	}
+
+	public function test_belongs_to_can_be_self_referential()
+	{
+		Author::$belongs_to = array(array('parent_author', 'class_name' => 'Author', 'foreign_key' => 'parent_author_id'));
+		$author = Author::find(1);
+		$this->assert_equals(1, $author->id);
+		$this->assert_equals(3, $author->parent_author->id);
+	}
+
+	public function test_belongs_to_with_an_invalid_option()
+	{
+		Event::$belongs_to[0]['joins'] = 'venue';
+		$event = Event::first()->venue;
+		$this->assert_sql_doesnt_has('INNER JOIN venues ON(events.venue_id = venues.id)',Event::table()->last_sql);
+	}
+
+	public function test_has_many_with_explicit_class_name()
+	{
+		Venue::$has_many = array(array('explicit_class_name', 'class_name' => 'Event', 'order' => 'id asc'));;
+		$this->assert_default_has_many($this->get_relationship(), 'explicit_class_name');
+	}
+
+	public function test_has_many_with_select()
+	{
+		Venue::$has_many[0]['select'] = 'title, type';
+		$venue = $this->get_relationship();
+		$this->assert_default_has_many($venue);
+
+		try {
+			$venue->events[0]->description;
+			$this->fail('expected Exception ActiveRecord\UndefinedPropertyException');
+		} catch (ActiveRecord\UndefinedPropertyException $e) {
+			$this->assert_true(strpos($e->getMessage(), 'description') !== false);
+		}
+	}
+
+	public function test_has_many_with_readonly()
+	{
+		Venue::$has_many[0]['readonly'] = true;
+		$venue = $this->get_relationship();
+		$this->assert_default_has_many($venue);
+
+		try {
+			$venue->events[0]->save();
+			$this->fail('expected exception ActiveRecord\ReadonlyException');
+		} catch (ActiveRecord\ReadonlyException $e) {
+		}
+
+		$venue->events[0]->description = 'new desc';
+		$this->assert_equals($venue->events[0]->description, 'new desc');
+	}
+
+	public function test_has_many_with_singular_attribute_name()
+	{
+		Venue::$has_many = array(array('event', 'class_name' => 'Event', 'order' => 'id asc'));
+		$this->assert_default_has_many($this->get_relationship(), 'event');
+	}
+
+	public function test_has_many_with_conditions_and_non_qualifying_record()
+	{
+		Venue::$has_many[0]['conditions'] = "title = 'pr0n @ railsconf'";
+		$venue = $this->get_relationship();
+		$this->assert_equals(2,$venue->id);
+		$this->assert_true(empty($venue->events), is_array($venue->events));
+	}
+
+	public function test_has_many_with_conditions_and_qualifying_record()
+	{
+		Venue::$has_many[0]['conditions'] = "title = 'Yeah Yeah Yeahs'";
+		$venue = $this->get_relationship();
+		$this->assert_equals(2,$venue->id);
+		$this->assert_equals($venue->events[0]->title,'Yeah Yeah Yeahs');
+	}
+
+	public function test_has_many_with_sql_clause_options()
+	{
+		Venue::$has_many[0] = array('events',
+			'select' => 'type',
+			'group'  => 'type',
+			'limit'  => 2,
+			'offset' => 1);
+		Venue::first()->events;
+		$this->assert_sql_has($this->conn->limit("SELECT type FROM events WHERE venue_id=? GROUP BY type",1,2),Event::table()->last_sql);
+	}
+
+	public function test_has_many_through()
+	{
+		$hosts = Venue::find(2)->hosts;
+		$this->assert_equals(2,$hosts[0]->id);
+		$this->assert_equals(3,$hosts[1]->id);
+	}
+
+	public function test_gh27_has_many_through_with_explicit_keys()
+	{
+		$property = Property::first();
+
+		$this->assert_equals(1, $property->amenities[0]->amenity_id);
+		$this->assert_equals(2, $property->amenities[1]->amenity_id);
+	}
+
+	public function test_gh16_has_many_through_inside_a_loop_should_not_cause_an_exception()
+	{
+		$count = 0;
+
+		foreach (Venue::all() as $venue)
+			$count += count($venue->hosts);
+
+		$this->assert_true($count >= 5);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\HasManyThroughAssociationException
+	 */
+	public function test_has_many_through_no_association()
+	{
+		Event::$belongs_to = array(array('host'));
+		Venue::$has_many[1] = array('hosts', 'through' => 'blahhhhhhh');
+
+		$venue = $this->get_relationship();
+		$n = $venue->hosts;
+		$this->assert_true(count($n) > 0);
+	}
+
+	public function test_has_many_through_with_select()
+	{
+		Event::$belongs_to = array(array('host'));
+		Venue::$has_many[1] = array('hosts', 'through' => 'events', 'select' => 'hosts.*, events.*');
+
+		$venue = $this->get_relationship();
+		$this->assert_true(count($venue->hosts) > 0);
+		$this->assert_not_null($venue->hosts[0]->title);
+	}
+
+	public function test_has_many_through_with_conditions()
+	{
+		Event::$belongs_to = array(array('host'));
+		Venue::$has_many[1] = array('hosts', 'through' => 'events', 'conditions' => array('events.title != ?', 'Love Overboard'));
+
+		$venue = $this->get_relationship();
+		$this->assert_true(count($venue->hosts) === 1);
+		$this->assert_sql_has("events.title !=",ActiveRecord\Table::load('Host')->last_sql);
+	}
+
+	public function test_has_many_through_using_source()
+	{
+		Event::$belongs_to = array(array('host'));
+		Venue::$has_many[1] = array('hostess', 'through' => 'events', 'source' => 'host');
+
+		$venue = $this->get_relationship();
+		$this->assert_true(count($venue->hostess) > 0);
+	}
+
+	/**
+	 * @expectedException ReflectionException
+	 */
+	public function test_has_many_through_with_invalid_class_name()
+	{
+		Event::$belongs_to = array(array('host'));
+		Venue::$has_one = array(array('invalid_assoc'));
+		Venue::$has_many[1] = array('hosts', 'through' => 'invalid_assoc');
+
+		$this->get_relationship()->hosts;
+	}
+
+	public function test_has_many_with_joins()
+	{
+		$x = Venue::first(array('joins' => array('events')));
+		$this->assert_sql_has('INNER JOIN events ON(venues.id = events.venue_id)',Venue::table()->last_sql);
+	}
+
+	public function test_has_many_with_explicit_keys()
+	{
+		$old = Author::$has_many;
+		Author::$has_many = array(array('explicit_books', 'class_name' => 'Book', 'primary_key' => 'parent_author_id', 'foreign_key' => 'secondary_author_id'));
+		$author = Author::find(4);
+
+		foreach ($author->explicit_books as $book)
+			$this->assert_equals($book->secondary_author_id, $author->parent_author_id);
+
+		$this->assert_true(strpos(ActiveRecord\Table::load('Book')->last_sql, "secondary_author_id") !== false);
+		Author::$has_many = $old;
+	}
+
+	public function test_has_one_basic()
+	{
+		$this->assert_default_has_one($this->get_relationship());
+	}
+
+	public function test_has_one_with_explicit_class_name()
+	{
+		Employee::$has_one = array(array('explicit_class_name', 'class_name' => 'Position'));
+		$this->assert_default_has_one($this->get_relationship(), 'explicit_class_name');
+	}
+
+	public function test_has_one_with_select()
+	{
+		Employee::$has_one[0]['select'] = 'title';
+		$employee = $this->get_relationship();
+		$this->assert_default_has_one($employee);
+
+		try {
+			$employee->position->active;
+			$this->fail('expected Exception ActiveRecord\UndefinedPropertyException');
+		} catch (ActiveRecord\UndefinedPropertyException $e) {
+			$this->assert_true(strpos($e->getMessage(), 'active') !== false);
+		}
+	}
+
+	public function test_has_one_with_order()
+	{
+		Employee::$has_one[0]['order'] = 'title';
+		$employee = $this->get_relationship();
+		$this->assert_default_has_one($employee);
+		$this->assert_sql_has('ORDER BY title',Position::table()->last_sql);
+	}
+
+	public function test_has_one_with_conditions_and_non_qualifying_record()
+	{
+		Employee::$has_one[0]['conditions'] = "title = 'programmer'";
+		$employee = $this->get_relationship();
+		$this->assert_equals(1,$employee->id);
+		$this->assert_null($employee->position);
+	}
+
+	public function test_has_one_with_conditions_and_qualifying_record()
+	{
+		Employee::$has_one[0]['conditions'] = "title = 'physicist'";
+		$this->assert_default_has_one($this->get_relationship());
+	}
+
+	public function test_has_one_with_readonly()
+	{
+		Employee::$has_one[0]['readonly'] = true;
+		$employee = $this->get_relationship();
+		$this->assert_default_has_one($employee);
+
+		try {
+			$employee->position->save();
+			$this->fail('expected exception ActiveRecord\ReadonlyException');
+		} catch (ActiveRecord\ReadonlyException $e) {
+		}
+
+		$employee->position->title = 'new title';
+		$this->assert_equals($employee->position->title, 'new title');
+	}
+
+	public function test_has_one_can_be_self_referential()
+	{
+		Author::$has_one[1] = array('parent_author', 'class_name' => 'Author', 'foreign_key' => 'parent_author_id');
+		$author = Author::find(1);
+		$this->assert_equals(1, $author->id);
+		$this->assert_equals(3, $author->parent_author->id);
+	}
+
+	public function test_has_one_with_joins()
+	{
+		$x = Employee::first(array('joins' => array('position')));
+		$this->assert_sql_has('INNER JOIN positions ON(employees.id = positions.employee_id)',Employee::table()->last_sql);
+	}
+
+	public function test_has_one_with_explicit_keys()
+	{
+		Book::$has_one = array(array('explicit_author', 'class_name' => 'Author', 'foreign_key' => 'parent_author_id', 'primary_key' => 'secondary_author_id'));
+
+		$book = Book::find(1);
+		$this->assert_equals($book->secondary_author_id, $book->explicit_author->parent_author_id);
+		$this->assert_true(strpos(ActiveRecord\Table::load('Author')->last_sql, "parent_author_id") !== false);
+	}
+
+	public function test_dont_attempt_to_load_if_all_foreign_keys_are_null()
+	{
+		$event = new Event();
+		$event->venue;
+		$this->assert_sql_doesnt_has($this->conn->last_query,'is IS NULL');
+	}
+
+	public function test_relationship_on_table_with_underscores()
+	{
+		$this->assert_equals(1,Author::find(1)->awesome_person->is_awesome);
+	}
+
+	public function test_has_one_through()
+	{
+		Venue::$has_many = array(array('events'),array('hosts', 'through' => 'events'));
+		$venue = Venue::first();
+		$this->assert_true(count($venue->hosts) > 0);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\RelationshipException
+	 */
+	public function test_throw_error_if_relationship_is_not_a_model()
+	{
+		AuthorWithNonModelRelationship::first()->books;
+	}
+
+	public function test_gh93_and_gh100_eager_loading_respects_association_options()
+	{
+		Venue::$has_many = array(array('events', 'class_name' => 'Event', 'order' => 'id asc', 'conditions' => array('length(title) = ?', 14)));
+		$venues = Venue::find(array(2, 6), array('include' => 'events'));
+
+		$this->assert_sql_has("WHERE length(title) = ? AND venue_id IN(?,?) ORDER BY id asc",ActiveRecord\Table::load('Event')->last_sql);
+		$this->assert_equals(1, count($venues[0]->events));
+    }
+
+	public function test_eager_loading_has_many_x()
+	{
+		$venues = Venue::find(array(2, 6), array('include' => 'events'));
+		$this->assert_sql_has("WHERE venue_id IN(?,?)",ActiveRecord\Table::load('Event')->last_sql);
+
+		foreach ($venues[0]->events as $event)
+			$this->assert_equals($event->venue_id, $venues[0]->id);
+
+		$this->assert_equals(2, count($venues[0]->events));
+	}
+
+	public function test_eager_loading_has_many_with_no_related_rows()
+	{
+		$venues = Venue::find(array(7, 8), array('include' => 'events'));
+
+		foreach ($venues as $v)
+			$this->assert_true(empty($v->events));
+
+		$this->assert_sql_has("WHERE id IN(?,?)",ActiveRecord\Table::load('Venue')->last_sql);
+		$this->assert_sql_has("WHERE venue_id IN(?,?)",ActiveRecord\Table::load('Event')->last_sql);
+	}
+
+	public function test_eager_loading_has_many_array_of_includes()
+	{
+		Author::$has_many = array(array('books'), array('awesome_people'));
+		$authors = Author::find(array(1,2), array('include' => array('books', 'awesome_people')));
+
+		$assocs = array('books', 'awesome_people');
+
+		foreach ($assocs as $assoc)
+		{
+			$this->assert_internal_type('array', $authors[0]->$assoc);
+
+			foreach ($authors[0]->$assoc as $a)
+				$this->assert_equals($authors[0]->author_id,$a->author_id);
+		}
+
+		foreach ($assocs as $assoc)
+		{
+			$this->assert_internal_type('array', $authors[1]->$assoc);
+			$this->assert_true(empty($authors[1]->$assoc));
+		}
+
+		$this->assert_sql_has("WHERE author_id IN(?,?)",ActiveRecord\Table::load('Author')->last_sql);
+		$this->assert_sql_has("WHERE author_id IN(?,?)",ActiveRecord\Table::load('Book')->last_sql);
+		$this->assert_sql_has("WHERE author_id IN(?,?)",ActiveRecord\Table::load('AwesomePerson')->last_sql);
+	}
+
+	public function test_eager_loading_has_many_nested()
+	{
+		$venues = Venue::find(array(1,2), array('include' => array('events' => array('host'))));
+
+		$this->assert_equals(2, count($venues));
+
+		foreach ($venues as $v)
+		{
+			$this->assert_true(count($v->events) > 0);
+
+			foreach ($v->events as $e)
+			{
+				$this->assert_equals($e->host_id, $e->host->id);
+				$this->assert_equals($v->id, $e->venue_id);
+			}
+		}
+
+		$this->assert_sql_has("WHERE id IN(?,?)",ActiveRecord\Table::load('Venue')->last_sql);
+		$this->assert_sql_has("WHERE venue_id IN(?,?)",ActiveRecord\Table::load('Event')->last_sql);
+		$this->assert_sql_has("WHERE id IN(?,?,?)",ActiveRecord\Table::load('Host')->last_sql);
+	}
+
+	public function test_eager_loading_belongs_to()
+	{
+		$events = Event::find(array(1,2,3,5,7), array('include' => 'venue'));
+
+		foreach ($events as $event)
+			$this->assert_equals($event->venue_id, $event->venue->id);
+
+		$this->assert_sql_has("WHERE id IN(?,?,?,?,?)",ActiveRecord\Table::load('Venue')->last_sql);
+	}
+
+	public function test_eager_loading_belongs_to_array_of_includes()
+	{
+		$events = Event::find(array(1,2,3,5,7), array('include' => array('venue', 'host')));
+
+		foreach ($events as $event)
+		{
+			$this->assert_equals($event->venue_id, $event->venue->id);
+			$this->assert_equals($event->host_id, $event->host->id);
+		}
+
+		$this->assert_sql_has("WHERE id IN(?,?,?,?,?)",ActiveRecord\Table::load('Event')->last_sql);
+		$this->assert_sql_has("WHERE id IN(?,?,?,?,?)",ActiveRecord\Table::load('Host')->last_sql);
+		$this->assert_sql_has("WHERE id IN(?,?,?,?,?)",ActiveRecord\Table::load('Venue')->last_sql);
+	}
+
+	public function test_eager_loading_belongs_to_nested()
+	{
+		Author::$has_many = array(array('awesome_people'));
+
+		$books = Book::find(array(1,2), array('include' => array('author' => array('awesome_people'))));
+
+		$assocs = array('author', 'awesome_people');
+
+		foreach ($books as $book)
+		{
+			$this->assert_equals($book->author_id,$book->author->author_id);
+			$this->assert_equals($book->author->author_id,$book->author->awesome_people[0]->author_id);
+		}
+
+		$this->assert_sql_has("WHERE book_id IN(?,?)",ActiveRecord\Table::load('Book')->last_sql);
+		$this->assert_sql_has("WHERE author_id IN(?,?)",ActiveRecord\Table::load('Author')->last_sql);
+		$this->assert_sql_has("WHERE author_id IN(?,?)",ActiveRecord\Table::load('AwesomePerson')->last_sql);
+	}
+
+	public function test_eager_loading_belongs_to_with_no_related_rows()
+	{
+		$e1 = Event::create(array('venue_id' => 200, 'host_id' => 200, 'title' => 'blah','type' => 'Music'));
+		$e2 = Event::create(array('venue_id' => 200, 'host_id' => 200, 'title' => 'blah2','type' => 'Music'));
+
+		$events = Event::find(array($e1->id, $e2->id), array('include' => 'venue'));
+
+		foreach ($events as $e)
+			$this->assert_null($e->venue);
+
+		$this->assert_sql_has("WHERE id IN(?,?)",ActiveRecord\Table::load('Event')->last_sql);
+		$this->assert_sql_has("WHERE id IN(?,?)",ActiveRecord\Table::load('Venue')->last_sql);
+	}
+
+	public function test_eager_loading_clones_related_objects()
+	{
+		$events = Event::find(array(2,3), array('include' => array('venue')));
+
+		$venue = $events[0]->venue;
+		$venue->name = "new name";
+
+		$this->assert_equals($venue->id, $events[1]->venue->id);
+		$this->assert_not_equals($venue->name, $events[1]->venue->name);
+		$this->assert_not_equals(spl_object_hash($venue), spl_object_hash($events[1]->venue));
+	}
+
+	public function test_eager_loading_clones_nested_related_objects()
+	{
+		$venues = Venue::find(array(1,2,6,9), array('include' => array('events' => array('host'))));
+
+		$unchanged_host = $venues[2]->events[0]->host;
+		$changed_host = $venues[3]->events[0]->host;
+		$changed_host->name = "changed";
+
+		$this->assert_equals($changed_host->id, $unchanged_host->id);
+		$this->assert_not_equals($changed_host->name, $unchanged_host->name);
+		$this->assert_not_equals(spl_object_hash($changed_host), spl_object_hash($unchanged_host));
+	}
+
+	public function test_gh_23_relationships_with_joins_to_same_table_should_alias_table_name()
+	{
+		$old = Book::$belongs_to;
+		Book::$belongs_to = array(
+			array('from_', 'class_name' => 'Author', 'foreign_key' => 'author_id'),
+			array('to', 'class_name' => 'Author', 'foreign_key' => 'secondary_author_id'),
+			array('another', 'class_name' => 'Author', 'foreign_key' => 'secondary_author_id')
+		);
+
+		$c = ActiveRecord\Table::load('Book')->conn;
+
+		$select = "books.*, authors.name as to_author_name, {$c->quote_name('from_')}.name as from_author_name, {$c->quote_name('another')}.name as another_author_name";
+		$book = Book::find(2, array('joins' => array('to', 'from_', 'another'),
+			'select' => $select));
+
+		$this->assert_not_null($book->from_author_name);
+		$this->assert_not_null($book->to_author_name);
+		$this->assert_not_null($book->another_author_name);
+		Book::$belongs_to = $old;
+	}
+
+	public function test_gh_40_relationships_with_joins_aliases_table_name_in_conditions()
+	{
+		$event = Event::find(1, array('joins' => array('venue')));
+
+		$this->assert_equals($event->id, $event->venue->id);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\RecordNotFound
+	 */
+	public function test_dont_attempt_eager_load_when_record_does_not_exist()
+	{
+		Author::find(999999, array('include' => array('books')));
+	}
+};
+?>

+ 283 - 0
hhvm/php-activerecord/test/SQLBuilderTest.php

@@ -0,0 +1,283 @@
+<?php
+include 'helpers/config.php';
+
+use ActiveRecord\SQLBuilder;
+use ActiveRecord\Table;
+
+class SQLBuilderTest extends DatabaseTest
+{
+	protected $table_name = 'authors';
+	protected $class_name = 'Author';
+	protected $table;
+
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+		$this->sql = new SQLBuilder($this->conn,$this->table_name);
+		$this->table = Table::load($this->class_name);
+	}
+
+	protected function cond_from_s($name, $values=null, $map=null)
+	{
+		return SQLBuilder::create_conditions_from_underscored_string($this->table->conn, $name, $values, $map);
+	}
+
+	public function assert_conditions($expected_sql, $values, $underscored_string, $map=null)
+	{
+		$cond = SQLBuilder::create_conditions_from_underscored_string($this->table->conn,$underscored_string,$values,$map);
+		$this->assert_sql_has($expected_sql,array_shift($cond));
+
+		if ($values)
+			$this->assert_equals(array_values(array_filter($values,function($s) { return $s !== null; })),array_values($cond));
+		else
+			$this->assert_equals(array(),$cond);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_no_connection()
+	{
+		new SQLBuilder(null,'authors');
+	}
+
+	public function test_nothing()
+	{
+		$this->assert_equals('SELECT * FROM authors',(string)$this->sql);
+	}
+
+	public function test_where_with_array()
+	{
+		$this->sql->where("id=? AND name IN(?)",1,array('Tito','Mexican'));
+		$this->assert_sql_has("SELECT * FROM authors WHERE id=? AND name IN(?,?)",(string)$this->sql);
+		$this->assert_equals(array(1,'Tito','Mexican'),$this->sql->get_where_values());
+	}
+
+	public function test_where_with_hash()
+	{
+		$this->sql->where(array('id' => 1, 'name' => 'Tito'));
+		$this->assert_sql_has("SELECT * FROM authors WHERE id=? AND name=?",(string)$this->sql);
+		$this->assert_equals(array(1,'Tito'),$this->sql->get_where_values());
+	}
+
+	public function test_where_with_hash_and_array()
+	{
+		$this->sql->where(array('id' => 1, 'name' => array('Tito','Mexican')));
+		$this->assert_sql_has("SELECT * FROM authors WHERE id=? AND name IN(?,?)",(string)$this->sql);
+		$this->assert_equals(array(1,'Tito','Mexican'),$this->sql->get_where_values());
+	}
+
+	public function test_gh134_where_with_hash_and_null()
+	{
+		$this->sql->where(array('id' => 1, 'name' => null));
+		$this->assert_sql_has("SELECT * FROM authors WHERE id=? AND name IS ?",(string)$this->sql);
+		$this->assert_equals(array(1, null),$this->sql->get_where_values());
+	}
+
+	public function test_where_with_null()
+	{
+		$this->sql->where(null);
+		$this->assert_equals('SELECT * FROM authors',(string)$this->sql);
+	}
+
+	public function test_where_with_no_args()
+	{
+		$this->sql->where();
+		$this->assert_equals('SELECT * FROM authors',(string)$this->sql);
+	}
+
+	public function test_order()
+	{
+		$this->sql->order('name');
+		$this->assert_equals('SELECT * FROM authors ORDER BY name',(string)$this->sql);
+	}
+
+	public function test_limit()
+	{
+		$this->sql->limit(10)->offset(1);
+		$this->assert_equals($this->conn->limit('SELECT * FROM authors',1,10),(string)$this->sql);
+	}
+
+	public function test_select()
+	{
+		$this->sql->select('id,name');
+		$this->assert_equals('SELECT id,name FROM authors',(string)$this->sql);
+	}
+
+	public function test_joins()
+	{
+		$join = 'inner join books on(authors.id=books.author_id)';
+		$this->sql->joins($join);
+		$this->assert_equals("SELECT * FROM authors $join",(string)$this->sql);
+	}
+
+	public function test_group()
+	{
+		$this->sql->group('name');
+		$this->assert_equals('SELECT * FROM authors GROUP BY name',(string)$this->sql);
+	}
+
+	public function test_having()
+	{
+		$this->sql->having("created_at > '2009-01-01'");
+		$this->assert_equals("SELECT * FROM authors HAVING created_at > '2009-01-01'", (string)$this->sql);
+	}
+
+	public function test_all_clauses_after_where_should_be_correctly_ordered()
+	{
+		$this->sql->limit(10)->offset(1);
+		$this->sql->having("created_at > '2009-01-01'");
+		$this->sql->order('name');
+		$this->sql->group('name');
+		$this->sql->where(array('id' => 1));
+		$this->assert_sql_has($this->conn->limit("SELECT * FROM authors WHERE id=? GROUP BY name HAVING created_at > '2009-01-01' ORDER BY name",1,10), (string)$this->sql);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ActiveRecordException
+	 */
+	public function test_insert_requires_hash()
+	{
+		$this->sql->insert(array(1));
+	}
+
+	public function test_insert()
+	{
+		$this->sql->insert(array('id' => 1, 'name' => 'Tito'));
+		$this->assert_sql_has("INSERT INTO authors(id,name) VALUES(?,?)",(string)$this->sql);
+	}
+
+	public function test_insert_with_null()
+	{
+		$this->sql->insert(array('id' => 1, 'name' => null));
+		$this->assert_sql_has("INSERT INTO authors(id,name) VALUES(?,?)",$this->sql->to_s());
+	}
+
+	public function test_update_with_hash()
+	{
+		$this->sql->update(array('id' => 1, 'name' => 'Tito'))->where('id=1 AND name IN(?)',array('Tito','Mexican'));
+ 		$this->assert_sql_has("UPDATE authors SET id=?, name=? WHERE id=1 AND name IN(?,?)",(string)$this->sql);
+		$this->assert_equals(array(1,'Tito','Tito','Mexican'),$this->sql->bind_values());
+	}
+
+	public function test_update_with_limit_and_order()
+	{
+		if (!$this->conn->accepts_limit_and_order_for_update_and_delete())
+			$this->mark_test_skipped('Only MySQL & Sqlite accept limit/order with UPDATE operation');
+
+		$this->sql->update(array('id' => 1))->order('name asc')->limit(1);
+		$this->assert_sql_has("UPDATE authors SET id=? ORDER BY name asc LIMIT 1", $this->sql->to_s());
+	}
+
+	public function test_update_with_string()
+	{
+		$this->sql->update("name='Bob'");
+		$this->assert_sql_has("UPDATE authors SET name='Bob'", $this->sql->to_s());
+	}
+
+	public function test_update_with_null()
+	{
+		$this->sql->update(array('id' => 1, 'name' => null))->where('id=1');
+		$this->assert_sql_has("UPDATE authors SET id=?, name=? WHERE id=1",$this->sql->to_s());
+	}
+
+	public function test_delete()
+	{
+		$this->sql->delete();
+		$this->assert_equals('DELETE FROM authors',$this->sql->to_s());
+	}
+
+	public function test_delete_with_where()
+	{
+		$this->sql->delete('id=? or name in(?)',1,array('Tito','Mexican'));
+		$this->assert_equals('DELETE FROM authors WHERE id=? or name in(?,?)',$this->sql->to_s());
+		$this->assert_equals(array(1,'Tito','Mexican'),$this->sql->bind_values());
+	}
+
+	public function test_delete_with_hash()
+	{
+		$this->sql->delete(array('id' => 1, 'name' => array('Tito','Mexican')));
+		$this->assert_sql_has("DELETE FROM authors WHERE id=? AND name IN(?,?)",$this->sql->to_s());
+		$this->assert_equals(array(1,'Tito','Mexican'),$this->sql->get_where_values());
+	}
+
+	public function test_delete_with_limit_and_order()
+	{
+		if (!$this->conn->accepts_limit_and_order_for_update_and_delete())
+			$this->mark_test_skipped('Only MySQL & Sqlite accept limit/order with DELETE operation');
+
+		$this->sql->delete(array('id' => 1))->order('name asc')->limit(1);
+		$this->assert_sql_has("DELETE FROM authors WHERE id=? ORDER BY name asc LIMIT 1",$this->sql->to_s());
+	}
+
+	public function test_reverse_order()
+	{
+		$this->assert_equals('id ASC, name DESC', SQLBuilder::reverse_order('id DESC, name ASC'));
+		$this->assert_equals('id ASC, name DESC , zzz ASC', SQLBuilder::reverse_order('id DESC, name ASC , zzz DESC'));
+		$this->assert_equals('id DESC, name DESC', SQLBuilder::reverse_order('id, name'));
+		$this->assert_equals('id DESC', SQLBuilder::reverse_order('id'));
+		$this->assert_equals('', SQLBuilder::reverse_order(''));
+		$this->assert_equals(' ', SQLBuilder::reverse_order(' '));
+		$this->assert_equals(null, SQLBuilder::reverse_order(null));
+	}
+
+	public function test_create_conditions_from_underscored_string()
+	{
+		$this->assert_conditions('id=? AND name=? OR z=?',array(1,'Tito','X'),'id_and_name_or_z');
+		$this->assert_conditions('id=?',array(1),'id');
+		$this->assert_conditions('id IN(?)',array(array(1,2)),'id');
+	}
+
+	public function test_create_conditions_from_underscored_string_with_nulls()
+	{
+		$this->assert_conditions('id=? AND name IS NULL',array(1,null),'id_and_name');
+	}
+
+	public function test_create_conditions_from_underscored_string_with_missing_args()
+	{
+		$this->assert_conditions('id=? AND name IS NULL OR z IS NULL',array(1,null),'id_and_name_or_z');
+		$this->assert_conditions('id IS NULL',null,'id');
+	}
+
+	public function test_create_conditions_from_underscored_string_with_blank()
+	{
+		$this->assert_conditions('id=? AND name IS NULL OR z=?',array(1,null,''),'id_and_name_or_z');
+	}
+
+	public function test_create_conditions_from_underscored_string_invalid()
+	{
+		$this->assert_equals(null,$this->cond_from_s(''));
+		$this->assert_equals(null,$this->cond_from_s(null));
+	}
+
+	public function test_create_conditions_from_underscored_string_with_mapped_columns()
+	{
+		$this->assert_conditions('id=? AND name=?',array(1,'Tito'),'id_and_my_name',array('my_name' => 'name'));
+	}
+
+	public function test_create_hash_from_underscored_string()
+	{
+		$values = array(1,'Tito');
+		$hash = SQLBuilder::create_hash_from_underscored_string('id_and_my_name',$values);
+		$this->assert_equals(array('id' => 1, 'my_name' => 'Tito'),$hash);
+	}
+
+	public function test_create_hash_from_underscored_string_with_mapped_columns()
+	{
+		$values = array(1,'Tito');
+		$map = array('my_name' => 'name');
+		$hash = SQLBuilder::create_hash_from_underscored_string('id_and_my_name',$values,$map);
+		$this->assert_equals(array('id' => 1, 'name' => 'Tito'),$hash);
+	}
+
+	public function test_where_with_joins_prepends_table_name_to_fields()
+	{
+		$joins = 'INNER JOIN books ON (books.id = authors.id)';
+		// joins needs to be called prior to where
+		$this->sql->joins($joins);
+		$this->sql->where(array('id' => 1, 'name' => 'Tito'));
+
+		$this->assert_sql_has("SELECT * FROM authors $joins WHERE authors.id=? AND authors.name=?",(string)$this->sql);
+	}
+};
+?>

+ 216 - 0
hhvm/php-activerecord/test/SerializationTest.php

@@ -0,0 +1,216 @@
+<?php
+include 'helpers/config.php';
+require '../lib/Serialization.php';
+
+use ActiveRecord\DateTime;
+
+class SerializationTest extends DatabaseTest
+{
+	public function tear_down()
+	{
+		parent::tear_down();
+		ActiveRecord\ArraySerializer::$include_root = false;
+		ActiveRecord\JsonSerializer::$include_root = false;
+	}
+
+	public function _a($options=array(), $model=null)
+	{
+		if (!$model)
+			$model = Book::find(1);
+
+		$s = new ActiveRecord\JsonSerializer($model,$options);
+		return $s->to_a();
+	}
+
+	public function test_only()
+	{
+		$this->assert_has_keys('name', 'special', $this->_a(array('only' => array('name', 'special'))));
+	}
+
+	public function test_only_not_array()
+	{
+		$this->assert_has_keys('name', $this->_a(array('only' => 'name')));
+	}
+
+	public function test_only_should_only_apply_to_attributes()
+	{
+		$this->assert_has_keys('name','author', $this->_a(array('only' => 'name', 'include' => 'author')));
+		$this->assert_has_keys('book_id','upper_name', $this->_a(array('only' => 'book_id', 'methods' => 'upper_name')));
+	}
+
+	public function test_only_overrides_except()
+	{
+		$this->assert_has_keys('name', $this->_a(array('only' => 'name', 'except' => 'name')));
+	}
+
+	public function test_except()
+	{
+		$this->assert_doesnt_has_keys('name', 'special', $this->_a(array('except' => array('name','special'))));
+	}
+
+	public function test_except_takes_a_string()
+	{
+		$this->assert_doesnt_has_keys('name', $this->_a(array('except' => 'name')));
+	}
+
+	public function test_methods()
+	{
+		$a = $this->_a(array('methods' => array('upper_name')));
+		$this->assert_equals('ANCIENT ART OF MAIN TANKING', $a['upper_name']);
+	}
+
+	public function test_methods_takes_a_string()
+	{
+		$a = $this->_a(array('methods' => 'upper_name'));
+		$this->assert_equals('ANCIENT ART OF MAIN TANKING', $a['upper_name']);
+	}
+
+	// methods added last should we shuld have value of the method in our json
+	// rather than the regular attribute value
+	public function test_methods_method_same_as_attribute()
+	{
+		$a = $this->_a(array('methods' => 'name'));
+		$this->assert_equals('ancient art of main tanking', $a['name']);
+	}
+
+	public function test_include()
+	{
+		$a = $this->_a(array('include' => array('author')));
+		$this->assert_has_keys('parent_author_id', $a['author']);
+	}
+
+	public function test_include_nested_with_nested_options()
+	{
+		$a = $this->_a(
+			array('include' => array('events' => array('except' => 'title', 'include' => array('host' => array('only' => 'id'))))),
+			Host::find(4));
+
+		$this->assert_equals(3, count($a['events']));
+		$this->assert_doesnt_has_keys('title', $a['events'][0]);
+		$this->assert_equals(array('id' => 4), $a['events'][0]['host']);
+	}
+
+	public function test_datetime_values_get_converted_to_strings()
+	{
+		$now = new DateTime();
+		$a = $this->_a(array('only' => 'created_at'),new Author(array('created_at' => $now)));
+		$this->assert_equals($now->format(ActiveRecord\Serialization::$DATETIME_FORMAT),$a['created_at']);
+	}
+
+	public function test_to_json()
+	{
+		$book = Book::find(1);
+		$json = $book->to_json();
+		$this->assert_equals($book->attributes(),(array)json_decode($json));
+	}
+
+	public function test_to_json_include_root()
+	{
+		ActiveRecord\JsonSerializer::$include_root = true;
+		$this->assert_not_null(json_decode(Book::find(1)->to_json())->book);
+	}
+
+	public function test_to_xml_include()
+	{
+		$xml = Host::find(4)->to_xml(array('include' => 'events'));
+		$decoded = get_object_vars(new SimpleXMLElement($xml));
+
+		$this->assert_equals(3, count($decoded['events']->event));
+	}
+
+	public function test_to_xml()
+	{
+		$book = Book::find(1);
+		$this->assert_equals($book->attributes(),get_object_vars(new SimpleXMLElement($book->to_xml())));
+	}
+
+  public function test_to_array()
+  {
+ 		$book = Book::find(1);
+		$array = $book->to_array();
+		$this->assert_equals($book->attributes(), $array);
+  }
+
+  public function test_to_array_include_root()
+  {
+		ActiveRecord\ArraySerializer::$include_root = true;
+ 		$book = Book::find(1);
+		$array = $book->to_array();
+    $book_attributes = array('book' => $book->attributes());
+		$this->assert_equals($book_attributes, $array);
+  }
+
+  public function test_to_array_except()
+  {
+ 		$book = Book::find(1);
+		$array = $book->to_array(array('except' => array('special')));
+		$book_attributes = $book->attributes();
+		unset($book_attributes['special']);
+		$this->assert_equals($book_attributes, $array);
+  }
+
+	public function test_works_with_datetime()
+	{
+		Author::find(1)->update_attribute('created_at',new DateTime());
+		$this->assert_reg_exp('/<updated_at>[0-9]{4}-[0-9]{2}-[0-9]{2}/',Author::find(1)->to_xml());
+		$this->assert_reg_exp('/"updated_at":"[0-9]{4}-[0-9]{2}-[0-9]{2}/',Author::find(1)->to_json());
+	}
+
+	public function test_to_xml_skip_instruct()
+	{
+		$this->assert_same(false,strpos(Book::find(1)->to_xml(array('skip_instruct' => true)),'<?xml version'));
+		$this->assert_same(0,    strpos(Book::find(1)->to_xml(array('skip_instruct' => false)),'<?xml version'));
+	}
+
+	public function test_only_method()
+	{
+		$this->assert_contains('<sharks>lasers</sharks>', Author::first()->to_xml(array('only_method' => 'return_something')));
+	}
+
+  public function test_to_csv()
+  {
+    $book = Book::find(1);
+    $this->assert_equals('1,1,2,"Ancient Art of Main Tanking",0,0',$book->to_csv());
+  }
+
+  public function test_to_csv_only_header()
+  {
+    $book = Book::find(1);
+    $this->assert_equals('book_id,author_id,secondary_author_id,name,numeric_test,special',
+                         $book->to_csv(array('only_header'=>true))
+                         );
+  }
+
+  public function test_to_csv_only_method()
+  {
+    $book = Book::find(1);
+    $this->assert_equals('2,"Ancient Art of Main Tanking"',
+                         $book->to_csv(array('only'=>array('name','secondary_author_id')))
+                         );
+  }
+
+  public function test_to_csv_only_method_on_header()
+  {
+    $book = Book::find(1);
+    $this->assert_equals('secondary_author_id,name',
+                         $book->to_csv(array('only'=>array('secondary_author_id','name'),
+                                             'only_header'=>true))
+                         );
+  }
+
+  public function test_to_csv_with_custom_delimiter()
+  {
+    $book = Book::find(1);
+    ActiveRecord\CsvSerializer::$delimiter=';';
+    $this->assert_equals('1;1;2;"Ancient Art of Main Tanking";0;0',$book->to_csv());
+  }
+
+  public function test_to_csv_with_custom_enclosure()
+  {
+    $book = Book::find(1);
+    ActiveRecord\CsvSerializer::$delimiter=',';
+    ActiveRecord\CsvSerializer::$enclosure="'";
+    $this->assert_equals("1,1,2,'Ancient Art of Main Tanking',0,0",$book->to_csv());
+  }
+};
+?>

+ 64 - 0
hhvm/php-activerecord/test/SqliteAdapterTest.php

@@ -0,0 +1,64 @@
+<?php
+include 'helpers/config.php';
+require_once __DIR__ . '/../lib/adapters/SqliteAdapter.php';
+
+class SqliteAdapterTest extends AdapterTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up('sqlite');
+	}
+
+	public function tearDown()
+	{
+		parent::tearDown();
+
+		@unlink($this->db);
+		@unlink(self::InvalidDb);
+	}
+
+	public function testConnectToInvalidDatabaseShouldNotCreateDbFile()
+	{
+		try
+		{
+			ActiveRecord\Connection::instance("sqlite://" . self::InvalidDb);
+			$this->assertFalse(true);
+		}
+		catch (ActiveRecord\DatabaseException $e)
+		{
+			$this->assertFalse(file_exists(__DIR__ . "/" . self::InvalidDb));
+		}
+	}
+
+	public function test_limit_with_null_offset_does_not_contain_offset()
+	{
+		$ret = array();
+		$sql = 'SELECT * FROM authors ORDER BY name ASC';
+		$this->conn->query_and_fetch($this->conn->limit($sql,null,1),function($row) use (&$ret) { $ret[] = $row; });
+
+		$this->assert_true(strpos($this->conn->last_query, 'LIMIT 1') !== false);
+	}
+
+	public function test_gh183_sqliteadapter_autoincrement()
+	{
+		// defined in lowercase: id integer not null primary key
+		$columns = $this->conn->columns('awesome_people');
+		$this->assert_true($columns['id']->auto_increment);
+
+		// defined in uppercase: `amenity_id` INTEGER NOT NULL PRIMARY KEY
+		$columns = $this->conn->columns('amenities');
+		$this->assert_true($columns['amenity_id']->auto_increment);
+
+		// defined using int: `rm-id` INT NOT NULL
+		$columns = $this->conn->columns('`rm-bldg`');
+		$this->assert_false($columns['rm-id']->auto_increment);
+
+		// defined using int: id INT NOT NULL PRIMARY KEY
+		$columns = $this->conn->columns('hosts');
+		$this->assert_true($columns['id']->auto_increment);
+	}
+
+	// not supported
+	public function test_connect_with_port() {}
+}
+?>

+ 108 - 0
hhvm/php-activerecord/test/UtilsTest.php

@@ -0,0 +1,108 @@
+<?php
+include 'helpers/config.php';
+
+use ActiveRecord as AR;
+
+class UtilsTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	public function set_up()
+	{
+		$this->object_array = array(null,null);
+		$this->object_array[0] = new stdClass();
+		$this->object_array[0]->a = "0a";
+		$this->object_array[0]->b = "0b";
+		$this->object_array[1] = new stdClass();
+		$this->object_array[1]->a = "1a";
+		$this->object_array[1]->b = "1b";
+
+		$this->array_hash = array(
+			array("a" => "0a", "b" => "0b"),
+			array("a" => "1a", "b" => "1b"));
+	}
+
+	public function test_collect_with_array_of_objects_using_closure()
+	{
+		$this->assert_equals(array("0a","1a"),AR\collect($this->object_array,function($obj) { return $obj->a; }));
+	}
+
+	public function test_collect_with_array_of_objects_using_string()
+	{
+		$this->assert_equals(array("0a","1a"),AR\collect($this->object_array,"a"));
+	}
+
+	public function test_collect_with_array_hash_using_closure()
+	{
+		$this->assert_equals(array("0a","1a"),AR\collect($this->array_hash,function($item) { return $item["a"]; }));
+	}
+
+	public function test_collect_with_array_hash_using_string()
+	{
+		$this->assert_equals(array("0a","1a"),AR\collect($this->array_hash,"a"));
+	}
+
+    public function test_array_flatten()
+    {
+		$this->assert_equals(array(), AR\array_flatten(array()));
+		$this->assert_equals(array(1), AR\array_flatten(array(1)));
+		$this->assert_equals(array(1), AR\array_flatten(array(array(1))));
+		$this->assert_equals(array(1, 2), AR\array_flatten(array(array(1, 2))));
+		$this->assert_equals(array(1, 2), AR\array_flatten(array(array(1), 2)));
+		$this->assert_equals(array(1, 2), AR\array_flatten(array(1, array(2))));
+		$this->assert_equals(array(1, 2, 3), AR\array_flatten(array(1, array(2), 3)));
+		$this->assert_equals(array(1, 2, 3, 4), AR\array_flatten(array(1, array(2, 3), 4)));
+		$this->assert_equals(array(1, 2, 3, 4, 5, 6), AR\array_flatten(array(1, array(2, 3), 4, array(5, 6))));
+	}
+
+	public function test_all()
+	{
+		$this->assert_true(AR\all(null,array(null,null)));
+		$this->assert_true(AR\all(1,array(1,1)));
+		$this->assert_false(AR\all(1,array(1,'1')));
+		$this->assert_false(AR\all(null,array('',null)));
+	}
+
+	public function test_classify()
+	{
+		$bad_class_names = array('ubuntu_rox', 'stop_the_Snake_Case', 'CamelCased', 'camelCased');
+		$good_class_names = array('UbuntuRox', 'StopTheSnakeCase', 'CamelCased', 'CamelCased');
+
+		$class_names = array();
+		foreach ($bad_class_names as $s)
+			$class_names[] = AR\classify($s);
+
+		$this->assert_equals($class_names, $good_class_names);
+	}
+
+	public function test_classify_singularize()
+	{
+		$bad_class_names = array('events', 'stop_the_Snake_Cases', 'angry_boxes', 'Mad_Sheep_herders', 'happy_People');
+		$good_class_names = array('Event', 'StopTheSnakeCase', 'AngryBox', 'MadSheepHerder', 'HappyPerson');
+
+		$class_names = array();
+		foreach ($bad_class_names as $s)
+			$class_names[] = AR\classify($s, true);
+
+		$this->assert_equals($class_names, $good_class_names);
+	}
+
+	public function test_singularize()
+	{
+		$this->assert_equals('order_status',AR\Utils::singularize('order_status'));
+		$this->assert_equals('order_status',AR\Utils::singularize('order_statuses'));
+		$this->assert_equals('os_type', AR\Utils::singularize('os_type'));
+		$this->assert_equals('os_type', AR\Utils::singularize('os_types'));
+		$this->assert_equals('photo', AR\Utils::singularize('photos'));
+		$this->assert_equals('pass', AR\Utils::singularize('pass'));
+		$this->assert_equals('pass', AR\Utils::singularize('passes'));
+	}
+
+	public function test_wrap_strings_in_arrays()
+	{
+		$x = array('1',array('2'));
+		$this->assert_equals(array(array('1'),array('2')),ActiveRecord\wrap_strings_in_arrays($x));
+
+		$x = '1';
+		$this->assert_equals(array(array('1')),ActiveRecord\wrap_strings_in_arrays($x));
+	}
+};
+?>

+ 112 - 0
hhvm/php-activerecord/test/ValidatesFormatOfTest.php

@@ -0,0 +1,112 @@
+<?php
+include 'helpers/config.php';
+
+class BookFormat extends ActiveRecord\Model
+{
+	static $table = 'books';
+	static $validates_format_of = array(
+		array('name')
+	);
+};
+
+class ValidatesFormatOfTest extends DatabaseTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+		BookFormat::$validates_format_of[0] = array('name');
+	}
+
+	public function test_format()
+	{
+		BookFormat::$validates_format_of[0]['with'] = '/^[a-z\W]*$/';
+		$book = new BookFormat(array('author_id' => 1, 'name' => 'testing reg'));
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+
+		BookFormat::$validates_format_of[0]['with'] = '/[0-9]/';
+		$book = new BookFormat(array('author_id' => 1, 'name' => 12));
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_invalid_null()
+	{
+		BookFormat::$validates_format_of[0]['with'] = '/[^0-9]/';
+		$book = new BookFormat;
+		$book->name = null;
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+	}
+
+	public function test_invalid_blank()
+	{
+		BookFormat::$validates_format_of[0]['with'] = '/[^0-9]/';
+		$book = new BookFormat;
+		$book->name = '';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+	}
+
+	public function test_valid_blank_andallow_blank()
+	{
+		BookFormat::$validates_format_of[0]['allow_blank'] = true;
+		BookFormat::$validates_format_of[0]['with'] = '/[^0-9]/';
+		$book = new BookFormat(array('author_id' => 1, 'name' => ''));
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_valid_null_and_allow_null()
+	{
+		BookFormat::$validates_format_of[0]['allow_null'] = true;
+		BookFormat::$validates_format_of[0]['with'] = '/[^0-9]/';
+		$book = new BookFormat();
+		$book->author_id = 1;
+		$book->name = null;
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ValidationsArgumentError
+	 */
+	public function test_invalid_lack_of_with_key()
+	{
+		$book = new BookFormat;
+		$book->name = null;
+		$book->save();
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ValidationsArgumentError
+	 */
+	public function test_invalid_with_expression_as_non_string()
+	{
+		BookFormat::$validates_format_of[0]['with'] = array('test');
+		$book = new BookFormat;
+		$book->name = null;
+		$book->save();
+	}
+
+	public function test_invalid_with_expression_as_non_regexp()
+	{
+		BookFormat::$validates_format_of[0]['with'] = 'blah';
+		$book = new BookFormat;
+		$book->name = 'blah';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+	}
+
+	public function test_custom_message()
+	{
+		BookFormat::$validates_format_of[0]['message'] = 'is using a custom message.';
+		BookFormat::$validates_format_of[0]['with'] = '/[^0-9]/';
+
+		$book = new BookFormat;
+		$book->name = null;
+		$book->save();
+		$this->assert_equals('is using a custom message.', $book->errors->on('name'));
+	}
+};
+?>

+ 158 - 0
hhvm/php-activerecord/test/ValidatesInclusionAndExclusionOfTest.php

@@ -0,0 +1,158 @@
+<?php
+include 'helpers/config.php';
+
+class BookExclusion extends ActiveRecord\Model
+{
+	static $table = 'books';
+	public static $validates_exclusion_of = array(
+		array('name', 'in' => array('blah', 'alpha', 'bravo'))
+	);
+};
+
+class BookInclusion extends ActiveRecord\Model
+{
+	static $table = 'books';
+	public static $validates_inclusion_of = array(
+		array('name', 'in' => array('blah', 'tanker', 'shark'))
+	);
+};
+
+class ValidatesInclusionAndExclusionOfTest extends DatabaseTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+		BookInclusion::$validates_inclusion_of[0] = array('name', 'in' => array('blah', 'tanker', 'shark'));
+		BookExclusion::$validates_exclusion_of[0] = array('name', 'in' => array('blah', 'alpha', 'bravo'));
+	}
+
+	public function test_inclusion()
+	{
+		$book = new BookInclusion;
+		$book->name = 'blah';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_exclusion()
+	{
+		$book = new BookExclusion;
+		$book->name = 'blahh';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_invalid_inclusion()
+	{
+		$book = new BookInclusion;
+		$book->name = 'thanker';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+		$book->name = 'alpha ';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+	}
+
+	public function test_invalid_exclusion()
+	{
+		$book = new BookExclusion;
+		$book->name = 'alpha';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+
+		$book = new BookExclusion;
+		$book->name = 'bravo';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+	}
+
+	public function test_inclusion_with_numeric()
+	{
+		BookInclusion::$validates_inclusion_of[0]['in']= array(0, 1, 2);
+		$book = new BookInclusion;
+		$book->name = 2;
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_inclusion_with_boolean()
+	{
+		BookInclusion::$validates_inclusion_of[0]['in']= array(true);
+		$book = new BookInclusion;
+		$book->name = true;
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_inclusion_with_null()
+	{
+		BookInclusion::$validates_inclusion_of[0]['in']= array(null);
+		$book = new BookInclusion;
+		$book->name = null;
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_invalid_inclusion_with_numeric()
+	{
+		BookInclusion::$validates_inclusion_of[0]['in']= array(0, 1, 2);
+		$book = new BookInclusion;
+		$book->name = 5;
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+	}
+
+	public function tes_inclusion_within_option()
+	{
+		BookInclusion::$validates_inclusion_of[0] = array('name', 'within' => array('okay'));
+		$book = new BookInclusion;
+		$book->name = 'okay';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function tes_inclusion_scalar_value()
+	{
+		BookInclusion::$validates_inclusion_of[0] = array('name', 'within' => 'okay');
+		$book = new BookInclusion;
+		$book->name = 'okay';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_valid_null()
+	{
+		BookInclusion::$validates_inclusion_of[0]['allow_null'] = true;
+		$book = new BookInclusion;
+		$book->name = null;
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_valid_blank()
+	{
+		BookInclusion::$validates_inclusion_of[0]['allow_blank'] = true;
+		$book = new BookInclusion;
+		$book->name = '';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_custom_message()
+	{
+		$msg = 'is using a custom message.';
+		BookInclusion::$validates_inclusion_of[0]['message'] = $msg;
+		BookExclusion::$validates_exclusion_of[0]['message'] = $msg;
+
+		$book = new BookInclusion;
+		$book->name = 'not included';
+		$book->save();
+		$this->assert_equals('is using a custom message.', $book->errors->on('name'));
+		$book = new BookExclusion;
+		$book->name = 'bravo';
+		$book->save();
+		$this->assert_equals('is using a custom message.', $book->errors->on('name'));
+	}
+
+};
+?>

+ 336 - 0
hhvm/php-activerecord/test/ValidatesLengthOfTest.php

@@ -0,0 +1,336 @@
+<?php
+include 'helpers/config.php';
+
+class BookLength extends ActiveRecord\Model
+{
+	static $table = 'books';
+	static $validates_length_of = array();
+}
+
+class BookSize extends ActiveRecord\Model
+{
+	static $table = 'books';
+	static $validates_size_of = array();
+}
+
+class ValidatesLengthOfTest extends DatabaseTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+		BookLength::$validates_length_of[0] = array('name', 'allow_blank' => false, 'allow_null' => false);
+	}
+	
+	public function test_within()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 5);
+		$book = new BookLength;
+		$book->name = '12345';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_within_error_message()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(2,5);
+		$book = new BookLength();
+		$book->name = '1';
+		$book->is_valid();
+		$this->assert_equals(array('Name is too short (minimum is 2 characters)'),$book->errors->full_messages());
+
+		$book->name = '123456';
+		$book->is_valid();
+		$this->assert_equals(array('Name is too long (maximum is 5 characters)'),$book->errors->full_messages());
+	}
+
+	public function test_within_custom_error_message()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(2,5);
+		BookLength::$validates_length_of[0]['too_short'] = 'is too short';
+		BookLength::$validates_length_of[0]['message'] = 'is not between 2 and 5 characters';
+		$book = new BookLength();
+		$book->name = '1';
+		$book->is_valid();
+		$this->assert_equals(array('Name is not between 2 and 5 characters'),$book->errors->full_messages());
+
+		$book->name = '123456';
+		$book->is_valid();
+		$this->assert_equals(array('Name is not between 2 and 5 characters'),$book->errors->full_messages());
+	}
+	
+	public function test_valid_in()
+	{
+		BookLength::$validates_length_of[0]['in'] = array(1, 5);
+		$book = new BookLength;
+		$book->name = '12345';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_aliased_size_of()
+	{
+		BookSize::$validates_size_of = BookLength::$validates_length_of;
+		BookSize::$validates_size_of[0]['within'] = array(1, 5);
+		$book = new BookSize;
+		$book->name = '12345';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_invalid_within_and_in()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 3);
+		$book = new BookLength;
+		$book->name = 'four';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+
+		$this->set_up();
+		BookLength::$validates_length_of[0]['in'] = array(1, 3);
+		$book = new BookLength;
+		$book->name = 'four';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+	}
+
+	public function test_valid_null()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 3);
+		BookLength::$validates_length_of[0]['allow_null'] = true;
+
+		$book = new BookLength;
+		$book->name = null;
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_valid_blank()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 3);
+		BookLength::$validates_length_of[0]['allow_blank'] = true;
+
+		$book = new BookLength;
+		$book->name = '';
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_invalid_blank()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 3);
+
+		$book = new BookLength;
+		$book->name = '';
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+		$this->assert_equals('is too short (minimum is 1 characters)', $book->errors->on('name'));
+	}
+
+	public function test_invalid_null_within()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 3);
+
+		$book = new BookLength;
+		$book->name = null;
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+		$this->assert_equals('is too short (minimum is 1 characters)', $book->errors->on('name'));
+	}
+	
+	public function test_invalid_null_minimum()
+	{
+		BookLength::$validates_length_of[0]['minimum'] = 1;
+
+		$book = new BookLength;
+		$book->name = null;
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+		$this->assert_equals('is too short (minimum is 1 characters)', $book->errors->on('name'));
+		
+	}
+	
+	public function test_valid_null_maximum()
+	{
+		BookLength::$validates_length_of[0]['maximum'] = 1;
+
+		$book = new BookLength;
+		$book->name = null;
+		$book->save();
+		$this->assert_false($book->errors->is_invalid('name'));
+	}
+
+	public function test_float_as_impossible_range_option()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 3.6);
+		$book = new BookLength;
+		$book->name = '123';
+		try {
+			$book->save();
+		} catch (ActiveRecord\ValidationsArgumentError $e) {
+			$this->assert_equals('maximum value cannot use a float for length.', $e->getMessage());
+		}
+
+		$this->set_up();
+		BookLength::$validates_length_of[0]['is'] = 1.8;
+		$book = new BookLength;
+		$book->name = '123';
+		try {
+			$book->save();
+		} catch (ActiveRecord\ValidationsArgumentError $e) {
+			$this->assert_equals('is value cannot use a float for length.', $e->getMessage());
+			return;
+		}
+
+		$this->fail('An expected exception has not be raised.');
+	}
+
+	public function test_signed_integer_as_impossible_within_option()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(-1, 3);
+
+		$book = new BookLength;
+		$book->name = '123';
+		try {
+			$book->save();
+		} catch (ActiveRecord\ValidationsArgumentError $e) {
+			$this->assert_equals('minimum value cannot use a signed integer.', $e->getMessage());
+			return;
+		}
+
+		$this->fail('An expected exception has not be raised.');
+	}
+
+	public function test_signed_integer_as_impossible_is_option()
+	{
+		BookLength::$validates_length_of[0]['is'] = -8;
+
+		$book = new BookLength;
+		$book->name = '123';
+		try {
+			$book->save();
+		} catch (ActiveRecord\ValidationsArgumentError $e) {
+			$this->assert_equals('is value cannot use a signed integer.', $e->getMessage());
+			return;
+		}
+
+		$this->fail('An expected exception has not be raised.');
+	}
+
+	public function test_lack_of_option()
+	{
+		try {
+			$book = new BookLength;
+			$book->name = null;
+			$book->save();
+		} catch (ActiveRecord\ValidationsArgumentError $e) {
+			$this->assert_equals('Range unspecified.  Specify the [within], [maximum], or [is] option.', $e->getMessage());
+			return;
+		}
+
+		$this->fail('An expected exception has not be raised.');
+	}
+
+	public function test_too_many_options()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 3);
+		BookLength::$validates_length_of[0]['in'] = array(1, 3);
+
+		try {
+			$book = new BookLength;
+			$book->name = null;
+			$book->save();
+		} catch (ActiveRecord\ValidationsArgumentError $e) {
+			$this->assert_equals('Too many range options specified.  Choose only one.', $e->getMessage());
+			return;
+		}
+
+		$this->fail('An expected exception has not be raised.');
+	}
+
+	public function test_too_many_options_with_different_option_types()
+	{
+		BookLength::$validates_length_of[0]['within'] = array(1, 3);
+		BookLength::$validates_length_of[0]['is'] = 3;
+
+		try {
+			$book = new BookLength;
+			$book->name = null;
+			$book->save();
+		} catch (ActiveRecord\ValidationsArgumentError $e) {
+			$this->assert_equals('Too many range options specified.  Choose only one.', $e->getMessage());
+			return;
+		}
+
+		$this->fail('An expected exception has not be raised.');
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ValidationsArgumentError
+	 */
+	public function test_with_option_as_non_numeric()
+	{
+		BookLength::$validates_length_of[0]['with'] = array('test');
+
+		$book = new BookLength;
+		$book->name = null;
+		$book->save();
+	}
+
+	/**
+	 * @expectedException ActiveRecord\ValidationsArgumentError
+	 */
+	public function test_with_option_as_non_numeric_non_array()
+	{
+		BookLength::$validates_length_of[0]['with'] = 'test';
+
+		$book = new BookLength;
+		$book->name = null;
+		$book->save();
+	}
+
+	public function test_validates_length_of_maximum()
+	{
+		BookLength::$validates_length_of[0] = array('name', 'maximum' => 10);
+		$book = new BookLength(array('name' => '12345678901'));
+		$book->is_valid();
+		$this->assert_equals(array("Name is too long (maximum is 10 characters)"),$book->errors->full_messages());
+	}
+
+	public function test_validates_length_of_minimum()
+	{
+		BookLength::$validates_length_of[0] = array('name', 'minimum' => 2);
+		$book = new BookLength(array('name' => '1'));
+		$book->is_valid();
+		$this->assert_equals(array("Name is too short (minimum is 2 characters)"),$book->errors->full_messages());
+	}
+	
+	public function test_validates_length_of_min_max_custom_message()
+	{
+		BookLength::$validates_length_of[0] = array('name', 'maximum' => 10, 'message' => 'is far too long');
+		$book = new BookLength(array('name' => '12345678901'));
+		$book->is_valid();
+		$this->assert_equals(array("Name is far too long"),$book->errors->full_messages());
+
+		BookLength::$validates_length_of[0] = array('name', 'minimum' => 10, 'message' => 'is far too short');
+		$book = new BookLength(array('name' => '123456789'));
+		$book->is_valid();
+		$this->assert_equals(array("Name is far too short"),$book->errors->full_messages());
+	}
+	
+	public function test_validates_length_of_min_max_custom_message_overridden()
+	{
+		BookLength::$validates_length_of[0] = array('name', 'minimum' => 10, 'too_short' => 'is too short', 'message' => 'is custom message');
+		$book = new BookLength(array('name' => '123456789'));
+		$book->is_valid();
+		$this->assert_equals(array("Name is custom message"),$book->errors->full_messages());
+	}
+
+	public function test_validates_length_of_is()
+	{
+		BookLength::$validates_length_of[0] = array('name', 'is' => 2);
+		$book = new BookLength(array('name' => '123'));
+		$book->is_valid();
+		$this->assert_equals(array("Name is the wrong length (should be 2 characters)"),$book->errors->full_messages());
+	}
+};
+?>

+ 166 - 0
hhvm/php-activerecord/test/ValidatesNumericalityOfTest.php

@@ -0,0 +1,166 @@
+<?php
+include 'helpers/config.php';
+
+class BookNumericality extends ActiveRecord\Model
+{
+	static $table_name = 'books';
+
+	static $validates_numericality_of = array(
+		array('name')
+	);
+}
+
+class ValidatesNumericalityOfTest extends DatabaseTest
+{
+	static $NULL = array(null);
+	static $BLANK = array("", " ", " \t \r \n");
+	static $FLOAT_STRINGS = array('0.0','+0.0','-0.0','10.0','10.5','-10.5','-0.0001','-090.1');
+	static $INTEGER_STRINGS = array('0', '+0', '-0', '10', '+10', '-10', '0090', '-090');
+	static $FLOATS = array(0.0, 10.0, 10.5, -10.5, -0.0001);
+	static $INTEGERS = array(0, 10, -10);
+	static $JUNK = array("not a number", "42 not a number", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number");
+
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+		BookNumericality::$validates_numericality_of = array(
+			array('numeric_test')
+		);
+	}
+
+	private function assert_validity($value, $boolean, $msg=null)
+	{
+		$book = new BookNumericality;
+		$book->numeric_test = $value;
+
+		if ($boolean == 'valid')
+		{
+			$this->assert_true($book->save());
+			$this->assert_false($book->errors->is_invalid('numeric_test'));
+		}
+		else
+		{
+			$this->assert_false($book->save());
+			$this->assert_true($book->errors->is_invalid('numeric_test'));
+
+			if (!is_null($msg))
+				$this->assert_same($msg, $book->errors->on('numeric_test'));
+		}
+	}
+
+	private function assert_invalid($values, $msg=null)
+	{
+		foreach ($values as $value)
+			$this->assert_validity($value, 'invalid', $msg);
+	}
+
+	private function assert_valid($values, $msg=null)
+	{
+		foreach ($values as $value)
+			$this->assert_validity($value, 'valid', $msg);
+	}
+
+	public function test_numericality()
+	{
+		//$this->assert_invalid(array("0xdeadbeef"));
+
+		$this->assert_valid(array_merge(self::$FLOATS, self::$INTEGERS));
+		$this->assert_invalid(array_merge(self::$NULL, self::$BLANK, self::$JUNK));
+	}
+
+	public function test_not_anumber()
+	{
+		$this->assert_invalid(array('blah'), 'is not a number');
+	}
+
+	public function test_invalid_null()
+	{
+		$this->assert_invalid(array(null));
+	}
+
+	public function test_invalid_blank()
+	{
+		$this->assert_invalid(array(' ', '  '), 'is not a number');
+	}
+
+	public function test_invalid_whitespace()
+	{
+		$this->assert_invalid(array(''));
+	}
+
+	public function test_valid_null()
+	{
+		BookNumericality::$validates_numericality_of[0]['allow_null'] = true;
+		$this->assert_valid(array(null));
+	}
+
+	public function test_only_integer()
+	{
+		BookNumericality::$validates_numericality_of[0]['only_integer'] = true;
+
+		$this->assert_valid(array(1, '1'));
+		$this->assert_invalid(array(1.5, '1.5'));
+	}
+
+	public function test_only_integer_matching_does_not_ignore_other_options()
+	{
+		BookNumericality::$validates_numericality_of[0]['only_integer'] = true;
+		BookNumericality::$validates_numericality_of[0]['greater_than'] = 0;
+
+		$this->assert_invalid(array(-1,'-1'));
+	}
+
+	public function test_greater_than()
+	{
+		BookNumericality::$validates_numericality_of[0]['greater_than'] = 5;
+
+		$this->assert_valid(array(6, '7'));
+		$this->assert_invalid(array(5, '5'), 'must be greater than 5');
+	}
+
+	public function test_greater_than_or_equal_to()
+	{
+		BookNumericality::$validates_numericality_of[0]['greater_than_or_equal_to'] = 5;
+
+		$this->assert_valid(array(5, 5.1, '5.1'));
+		$this->assert_invalid(array(-50, 4.9, '4.9','-5.1'));
+	}
+
+	public function test_less_than()
+	{
+		BookNumericality::$validates_numericality_of[0]['less_than'] = 5;
+
+		$this->assert_valid(array(4.9, -1, 0, '-5'));
+		$this->assert_invalid(array(5, '5'), 'must be less than 5');
+	}
+
+	public function test_less_than_or_equal_to()
+	{
+		BookNumericality::$validates_numericality_of[0]['less_than_or_equal_to'] = 5;
+
+		$this->assert_valid(array(5, -1, 0, 4.9, '-5'));
+		$this->assert_invalid(array('8', 5.1), 'must be less than or equal to 5');
+	}
+
+	public function test_greater_than_less_than_and_even()
+	{
+		BookNumericality::$validates_numericality_of[0] = array('numeric_test', 'greater_than' => 1, 'less_than' => 4, 'even' => true);
+
+		$this->assert_valid(array(2));
+		$this->assert_invalid(array(1,3,4));
+	}
+
+	public function test_custom_message()
+	{
+		BookNumericality::$validates_numericality_of = array(
+			array('numeric_test', 'message' => 'Hello')
+		);
+		$book = new BookNumericality(array('numeric_test' => 'NaN'));
+		$book->is_valid();
+		$this->assert_equals(array('Numeric test Hello'),$book->errors->full_messages());
+	}
+};
+
+array_merge(ValidatesNumericalityOfTest::$INTEGERS, ValidatesNumericalityOfTest::$INTEGER_STRINGS);
+array_merge(ValidatesNumericalityOfTest::$FLOATS, ValidatesNumericalityOfTest::$FLOAT_STRINGS);
+?>

+ 75 - 0
hhvm/php-activerecord/test/ValidatesPresenceOfTest.php

@@ -0,0 +1,75 @@
+<?php
+include 'helpers/config.php';
+
+class BookPresence extends ActiveRecord\Model
+{
+	static $table_name = 'books';
+
+	static $validates_presence_of = array(
+		array('name')
+	);
+}
+
+class AuthorPresence extends ActiveRecord\Model
+{
+	static $table_name = 'authors';
+
+	static $validates_presence_of = array(
+		array('some_date')
+	);
+};
+
+class ValidatesPresenceOfTest extends DatabaseTest
+{
+	public function test_presence()
+	{
+		$book = new BookPresence(array('name' => 'blah'));
+		$this->assert_false($book->is_invalid());
+	}
+
+	public function test_presence_on_date_field_is_valid()
+	{
+		$author = new AuthorPresence(array('some_date' => '2010-01-01'));
+		$this->assert_true($author->is_valid());
+	}
+
+	public function test_presence_on_date_field_is_not_valid()
+	{
+		$author = new AuthorPresence();
+		$this->assert_false($author->is_valid());
+	}
+	
+	public function test_invalid_null()
+	{
+		$book = new BookPresence(array('name' => null));
+		$this->assert_true($book->is_invalid());
+	}
+
+	public function test_invalid_blank()
+	{
+		$book = new BookPresence(array('name' => ''));
+		$this->assert_true($book->is_invalid());
+	}
+
+	public function test_valid_white_space()
+	{
+		$book = new BookPresence(array('name' => ' '));
+		$this->assert_false($book->is_invalid());
+	}
+
+	public function test_custom_message()
+	{
+		BookPresence::$validates_presence_of[0]['message'] = 'is using a custom message.';
+
+		$book = new BookPresence(array('name' => null));
+		$book->is_valid();
+		$this->assert_equals('is using a custom message.', $book->errors->on('name'));
+	}
+
+	public function test_valid_zero()
+	{
+		$book = new BookPresence(array('name' => 0));
+		$this->assert_true($book->is_valid());
+	}
+};
+?>

+ 183 - 0
hhvm/php-activerecord/test/ValidationsTest.php

@@ -0,0 +1,183 @@
+<?php
+include 'helpers/config.php';
+
+use ActiveRecord as AR;
+
+class BookValidations extends ActiveRecord\Model
+{
+	static $table_name = 'books';
+	static $alias_attribute = array('name_alias' => 'name', 'x' => 'secondary_author_id');
+	static $validates_presence_of = array();
+	static $validates_uniqueness_of = array();
+	static $custom_validator_error_msg = 'failed custom validation';
+
+	// fired for every validation - but only used for custom validation test
+	public function validate()
+	{
+		if ($this->name == 'test_custom_validation')
+			$this->errors->add('name', self::$custom_validator_error_msg);
+	}
+}
+
+class ValuestoreValidations extends ActiveRecord\Model
+{
+	static $table_name = 'valuestore';
+	static $validates_uniqueness_of = array();
+}
+
+class ValidationsTest extends DatabaseTest
+{
+	public function set_up($connection_name=null)
+	{
+		parent::set_up($connection_name);
+
+		BookValidations::$validates_presence_of[0] = 'name';
+		BookValidations::$validates_uniqueness_of[0] = 'name';
+		
+		ValuestoreValidations::$validates_uniqueness_of[0] = 'key';
+	}
+
+	public function test_is_valid_invokes_validations()
+	{
+		$book = new Book;
+		$this->assert_true(empty($book->errors));
+		$book->is_valid();
+		$this->assert_false(empty($book->errors));
+	}
+
+	public function test_is_valid_returns_true_if_no_validations_exist()
+	{
+		$book = new Book;
+		$this->assert_true($book->is_valid());
+	}
+
+	public function test_is_valid_returns_false_if_failed_validations()
+	{
+		$book = new BookValidations;
+		$this->assert_false($book->is_valid());
+	}
+
+	public function test_is_invalid()
+	{
+		$book = new Book();
+		$this->assert_false($book->is_invalid());
+	}
+
+	public function test_is_invalid_is_true()
+	{
+		$book = new BookValidations();
+		$this->assert_true($book->is_invalid());
+	}
+
+	public function test_is_iterable()
+	{
+		$book = new BookValidations();
+		$book->is_valid();
+
+		foreach ($book->errors as $name => $message)
+			$this->assert_equals("Name can't be blank",$message);
+	}
+
+	public function test_full_messages()
+	{
+		$book = new BookValidations();
+		$book->is_valid();
+
+		$this->assert_equals(array("Name can't be blank"),array_values($book->errors->full_messages(array('hash' => true))));
+	}
+
+	public function test_to_array()
+	{
+		$book = new BookValidations();
+		$book->is_valid();
+
+		$this->assert_equals(array("name" => array("Name can't be blank")), $book->errors->to_array());
+	}
+	
+	public function test_toString()
+	{
+		$book = new BookValidations();
+		$book->is_valid();
+		$book->errors->add('secondary_author_id', "is invalid");
+		
+		$this->assert_equals("Name can't be blank\nSecondary author id is invalid", (string) $book->errors);
+	}
+
+	public function test_validates_uniqueness_of()
+	{
+		BookValidations::create(array('name' => 'bob'));
+		$book = BookValidations::create(array('name' => 'bob'));
+
+		$this->assert_equals(array("Name must be unique"),$book->errors->full_messages());
+		$this->assert_equals(1,BookValidations::count(array('conditions' => "name='bob'")));
+	}
+
+	public function test_validates_uniqueness_of_excludes_self()
+	{
+		$book = BookValidations::first();
+		$this->assert_equals(true,$book->is_valid());
+	}
+
+	public function test_validates_uniqueness_of_with_multiple_fields()
+	{
+		BookValidations::$validates_uniqueness_of[0] = array(array('name','special'));
+		$book1 = BookValidations::first();
+		$book2 = new BookValidations(array('name' => $book1->name, 'special' => $book1->special+1));
+		$this->assert_true($book2->is_valid());
+	}
+
+	public function test_validates_uniqueness_of_with_multiple_fields_is_not_unique()
+	{
+		BookValidations::$validates_uniqueness_of[0] = array(array('name','special'));
+		$book1 = BookValidations::first();
+		$book2 = new BookValidations(array('name' => $book1->name, 'special' => $book1->special));
+		$this->assert_false($book2->is_valid());
+		$this->assert_equals(array('Name and special must be unique'),$book2->errors->full_messages());
+	}
+
+	public function test_validates_uniqueness_of_works_with_alias_attribute()
+	{
+		BookValidations::$validates_uniqueness_of[0] = array(array('name_alias','x'));
+		$book = BookValidations::create(array('name_alias' => 'Another Book', 'x' => 2));
+		$this->assert_false($book->is_valid());
+		$this->assert_equals(array('Name alias and x must be unique'), $book->errors->full_messages());
+	}
+
+	public function test_validates_uniqueness_of_works_with_mysql_reserved_word_as_column_name()
+	{
+		ValuestoreValidations::create(array('key' => 'GA_KEY', 'value' => 'UA-1234567-1'));
+		$valuestore = ValuestoreValidations::create(array('key' => 'GA_KEY', 'value' => 'UA-1234567-2'));
+
+		$this->assert_equals(array("Key must be unique"),$valuestore->errors->full_messages());
+		$this->assert_equals(1,ValuestoreValidations::count(array('conditions' => "`key`='GA_KEY'")));
+	}
+
+	public function test_get_validation_rules()
+	{
+		$validators = BookValidations::first()->get_validation_rules();
+		$this->assert_true(in_array(array('validator' => 'validates_presence_of'),$validators['name']));
+	}
+
+	public function test_model_is_nulled_out_to_prevent_memory_leak()
+	{
+		$book = new BookValidations();
+		$book->is_valid();
+		$this->assert_true(strpos(serialize($book->errors),'model";N;') !== false);
+	}
+
+	public function test_validations_takes_strings()
+	{
+		BookValidations::$validates_presence_of = array('numeric_test', array('special'), 'name');
+		$book = new BookValidations(array('numeric_test' => 1, 'special' => 1));
+		$this->assert_false($book->is_valid());
+	}
+
+	public function test_gh131_custom_validation()
+	{
+		$book = new BookValidations(array('name' => 'test_custom_validation'));
+		$book->save();
+		$this->assert_true($book->errors->is_invalid('name'));
+		$this->assert_equals(BookValidations::$custom_validator_error_msg, $book->errors->on('name'));
+	}
+};
+?>

+ 4 - 0
hhvm/php-activerecord/test/fixtures/amenities.csv

@@ -0,0 +1,4 @@
+amenity_id, type
+1, "Test #1"
+2, "Test #2"
+3, "Test #3"

+ 5 - 0
hhvm/php-activerecord/test/fixtures/authors.csv

@@ -0,0 +1,5 @@
+author_id,parent_author_id,name
+1,3,"Tito"
+2,2,"George W. Bush"
+3,1,"Bill Clinton"
+4,2,"Uncle Bob"

+ 4 - 0
hhvm/php-activerecord/test/fixtures/awesome_people.csv

@@ -0,0 +1,4 @@
+id,author_id
+1,1
+2,2
+3,3

+ 3 - 0
hhvm/php-activerecord/test/fixtures/books.csv

@@ -0,0 +1,3 @@
+book_id,author_id,secondary_author_id,name,special
+1,1,2,"Ancient Art of Main Tanking",0
+2,2,2,"Another Book",0

+ 4 - 0
hhvm/php-activerecord/test/fixtures/employees.csv

@@ -0,0 +1,4 @@
+id,first_name,last_name,nick_name
+1,"michio","kaku","kakz"
+2,"jacques","fuentes","jax"
+3,"kien","la","kla"

+ 7 - 0
hhvm/php-activerecord/test/fixtures/events.csv

@@ -0,0 +1,7 @@
+id,venue_id,host_id,title,description,type
+1,1,1,"Monday Night Music Club feat. The Shivers","","Music"
+2,2,2,"Yeah Yeah Yeahs","","Music"
+3,2,3,"Love Overboard","","Music"
+5,6,4,"1320 Records Presents A \"Live PA Set\" By STS9 with",,"Music"
+6,500,4,"Kla likes to dance to YMCA","","Music"
+7,9,4,"Blah",,"Blah"

+ 5 - 0
hhvm/php-activerecord/test/fixtures/hosts.csv

@@ -0,0 +1,5 @@
+id,name
+1,"David Letterman"
+2,"Billy Crystal"
+3,"Jon Stewart"
+4,"Funny Guy"

+ 2 - 0
hhvm/php-activerecord/test/fixtures/newsletters.csv

@@ -0,0 +1,2 @@
+id
+1

+ 4 - 0
hhvm/php-activerecord/test/fixtures/positions.csv

@@ -0,0 +1,4 @@
+id,employee_id,title,active
+3,1,"physicist",0
+2,2,"programmer",1
+1,3,"programmer",1

+ 3 - 0
hhvm/php-activerecord/test/fixtures/property.csv

@@ -0,0 +1,3 @@
+property_id
+28840
+28841

+ 5 - 0
hhvm/php-activerecord/test/fixtures/property_amenities.csv

@@ -0,0 +1,5 @@
+id, amenity_id, property_id
+257117, 1, 28840
+257118, 2, 28840
+257119, 2, 28841
+257120, 3, 28841

+ 2 - 0
hhvm/php-activerecord/test/fixtures/rm-bldg.csv

@@ -0,0 +1,2 @@
+rm-id,rm-name,"space out"
+1,"name","x"

+ 2 - 0
hhvm/php-activerecord/test/fixtures/user_newsletters.csv

@@ -0,0 +1,2 @@
+id,user_id,newsletter_id
+1,1,1

+ 2 - 0
hhvm/php-activerecord/test/fixtures/users.csv

@@ -0,0 +1,2 @@
+id
+1

+ 0 - 0
hhvm/php-activerecord/test/fixtures/valuestore.csv


+ 7 - 0
hhvm/php-activerecord/test/fixtures/venues.csv

@@ -0,0 +1,7 @@
+id,name,city,state,address,phone
+1,"Blender Theater at Gramercy","New York","NY","127 East 23rd Street","2127776800"
+2,"Warner Theatre","Washington","DC","1299 Pennsylvania Ave NW","2027834000"
+6,"The Note - West Chester","West Chester","PA","142 E. Market St.","0000000000"
+7,"The National","Richmond","VA","708 East Broad Street","1112223333"
+8,"Hampton Coliseum","Hampton","VA","1000 Coliseum Dr","2223334444"
+9,"YMCA","Washington","DC","1234 YMCA Way","2222222222"

+ 405 - 0
hhvm/php-activerecord/test/helpers/AdapterTest.php

@@ -0,0 +1,405 @@
+<?php
+use ActiveRecord\Column;
+
+class AdapterTest extends DatabaseTest
+{
+	const InvalidDb = '__1337__invalid_db__';
+
+	public function set_up($connection_name=null)
+	{
+		if (($connection_name && !in_array($connection_name, PDO::getAvailableDrivers())) ||
+			ActiveRecord\Config::instance()->get_connection($connection_name) == 'skip')
+			$this->mark_test_skipped($connection_name . ' drivers are not present');
+
+		parent::set_up($connection_name);
+	}
+
+	public function test_i_has_a_default_port_unless_im_sqlite()
+	{
+		if ($this->conn instanceof ActiveRecord\SqliteAdapter)
+			return;
+
+		$c = $this->conn;
+		$this->assert_true($c::$DEFAULT_PORT > 0);
+	}
+
+	public function test_should_set_adapter_variables()
+	{
+		$this->assert_not_null($this->conn->protocol);
+	}
+
+	public function test_null_connection_string_uses_default_connection()
+	{
+		$this->assert_not_null(ActiveRecord\Connection::instance(null));
+		$this->assert_not_null(ActiveRecord\Connection::instance(''));
+		$this->assert_not_null(ActiveRecord\Connection::instance());
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_invalid_connection_protocol()
+	{
+		ActiveRecord\Connection::instance('terribledb://user:pass@host/db');
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_no_host_connection()
+	{
+		if (!$GLOBALS['slow_tests'])
+			throw new ActiveRecord\DatabaseException("");
+
+		ActiveRecord\Connection::instance("{$this->conn->protocol}://user:pass");
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_connection_failed_invalid_host()
+	{
+		if (!$GLOBALS['slow_tests'])
+			throw new ActiveRecord\DatabaseException("");
+
+		ActiveRecord\Connection::instance("{$this->conn->protocol}://user:pass/1.1.1.1/db");
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_connection_failed()
+	{
+		ActiveRecord\Connection::instance("{$this->conn->protocol}://baduser:[email protected]/db");
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_connect_failed()
+	{
+		ActiveRecord\Connection::instance("{$this->conn->protocol}://zzz:[email protected]/test");
+	}
+
+	public function test_connect_with_port()
+	{
+		$config = ActiveRecord\Config::instance();
+		$name = $config->get_default_connection();
+		$url = parse_url($config->get_connection($name));
+		$conn = $this->conn;
+		$port = $conn::$DEFAULT_PORT;
+
+		if ($this->conn->protocol != 'sqlite')
+			ActiveRecord\Connection::instance("{$url['scheme']}://{$url['user']}:{$url['pass']}@{$url['host']}:$port{$url['path']}");
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_connect_to_invalid_database()
+	{
+		ActiveRecord\Connection::instance("{$this->conn->protocol}://test:[email protected]/" . self::InvalidDb);
+	}
+
+	public function test_date_time_type()
+	{
+		$columns = $this->conn->columns('authors');
+		$this->assert_equals('datetime',$columns['created_at']->raw_type);
+		$this->assert_equals(Column::DATETIME,$columns['created_at']->type);
+		$this->assert_true($columns['created_at']->length > 0);
+	}
+
+	public function test_date()
+	{
+		$columns = $this->conn->columns('authors');
+		$this->assert_equals('date', $columns['some_Date']->raw_type);
+		$this->assert_equals(Column::DATE, $columns['some_Date']->type);
+		$this->assert_true($columns['some_Date']->length >= 7);
+	}
+
+	public function test_columns_no_inflection_on_hash_key()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_true(array_key_exists('author_id',$author_columns));
+	}
+
+	public function test_columns_nullable()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_false($author_columns['author_id']->nullable);
+		$this->assert_true($author_columns['parent_author_id']->nullable);
+	}
+
+	public function test_columns_pk()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_true($author_columns['author_id']->pk);
+		$this->assert_false($author_columns['parent_author_id']->pk);
+	}
+
+	public function test_columns_sequence()
+	{
+		if ($this->conn->supports_sequences())
+		{
+			$author_columns = $this->conn->columns('authors');
+			$this->assert_equals('authors_author_id_seq',$author_columns['author_id']->sequence);
+		}
+	}
+
+	public function test_columns_default()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_equals('default_name',$author_columns['name']->default);
+	}
+
+	public function test_columns_type()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_equals('varchar',substr($author_columns['name']->raw_type,0,7));
+		$this->assert_equals(Column::STRING,$author_columns['name']->type);
+		$this->assert_equals(25,$author_columns['name']->length);
+	}
+
+	public function test_columns_text()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_equals('text',$author_columns['some_text']->raw_type);
+		$this->assert_equals(null,$author_columns['some_text']->length);
+	}
+
+	public function test_columns_time()
+	{
+		$author_columns = $this->conn->columns('authors');
+		$this->assert_equals('time',$author_columns['some_time']->raw_type);
+		$this->assert_equals(Column::TIME,$author_columns['some_time']->type);
+	}
+
+	public function test_query()
+	{
+		$sth = $this->conn->query('SELECT * FROM authors');
+
+		while (($row = $sth->fetch()))
+			$this->assert_not_null($row);
+
+		$sth = $this->conn->query('SELECT * FROM authors WHERE author_id=1');
+		$row = $sth->fetch();
+		$this->assert_equals('Tito',$row['name']);
+	}
+
+	/**
+	 * @expectedException ActiveRecord\DatabaseException
+	 */
+	public function test_invalid_query()
+	{
+		$this->conn->query('alsdkjfsdf');
+	}
+
+	public function test_fetch()
+	{
+		$sth = $this->conn->query('SELECT * FROM authors WHERE author_id IN(1,2,3)');
+		$i = 0;
+		$ids = array();
+
+		while (($row = $sth->fetch()))
+		{
+			++$i;
+			$ids[] = $row['author_id'];
+		}
+
+		$this->assert_equals(3,$i);
+		$this->assert_equals(array(1,2,3),$ids);
+	}
+
+	public function test_query_with_params()
+	{
+		$x=array('Bill Clinton','Tito');
+		$sth = $this->conn->query('SELECT * FROM authors WHERE name IN(?,?) ORDER BY name DESC',$x);
+		$row = $sth->fetch();
+		$this->assert_equals('Tito',$row['name']);
+
+		$row = $sth->fetch();
+		$this->assert_equals('Bill Clinton',$row['name']);
+
+		$row = $sth->fetch();
+		$this->assert_equals(null,$row);
+	}
+
+	public function test_insert_id_should_return_explicitly_inserted_id()
+	{
+		$this->conn->query('INSERT INTO authors(author_id,name) VALUES(99,\'name\')');
+		$this->assert_true($this->conn->insert_id() > 0);
+	}
+
+	public function test_insert_id()
+	{
+		$this->conn->query("INSERT INTO authors(name) VALUES('name')");
+		$this->assert_true($this->conn->insert_id() > 0);
+	}
+
+	public function test_insert_id_with_params()
+	{
+		$x = array('name');
+		$this->conn->query('INSERT INTO authors(name) VALUES(?)',$x);
+		$this->assert_true($this->conn->insert_id() > 0);
+	}
+
+	public function test_inflection()
+	{
+		$columns = $this->conn->columns('authors');
+		$this->assert_equals('parent_author_id',$columns['parent_author_id']->inflected_name);
+	}
+
+	public function test_escape()
+	{
+		$s = "Bob's";
+		$this->assert_not_equals($s,$this->conn->escape($s));
+	}
+
+	public function test_columnsx()
+	{
+		$columns = $this->conn->columns('authors');
+		$names = array('author_id','parent_author_id','name','updated_at','created_at','some_Date','some_time','some_text','encrypted_password','mixedCaseField');
+
+		if ($this->conn instanceof ActiveRecord\OciAdapter)
+			$names = array_filter(array_map('strtolower',$names),function($s) { $s !== 'some_time'; });
+
+		foreach ($names as $field)
+			$this->assert_true(array_key_exists($field,$columns));
+
+		$this->assert_equals(true,$columns['author_id']->pk);
+		$this->assert_equals('int',$columns['author_id']->raw_type);
+		$this->assert_equals(Column::INTEGER,$columns['author_id']->type);
+		$this->assert_true($columns['author_id']->length > 1);
+		$this->assert_false($columns['author_id']->nullable);
+
+		$this->assert_equals(false,$columns['parent_author_id']->pk);
+		$this->assert_true($columns['parent_author_id']->nullable);
+
+		$this->assert_equals('varchar',substr($columns['name']->raw_type,0,7));
+		$this->assert_equals(Column::STRING,$columns['name']->type);
+		$this->assert_equals(25,$columns['name']->length);
+	}
+
+	public function test_columns_decimal()
+	{
+		$columns = $this->conn->columns('books');
+		$this->assert_equals(Column::DECIMAL,$columns['special']->type);
+		$this->assert_true($columns['special']->length >= 10);
+	}
+
+	private function limit($offset, $limit)
+	{
+		$ret = array();
+		$sql = 'SELECT * FROM authors ORDER BY name ASC';
+		$this->conn->query_and_fetch($this->conn->limit($sql,$offset,$limit),function($row) use (&$ret) { $ret[] = $row; });
+		return ActiveRecord\collect($ret,'author_id');
+	}
+
+	public function test_limit()
+	{
+		$this->assert_equals(array(2,1),$this->limit(1,2));
+	}
+
+	public function test_limit_to_first_record()
+	{
+		$this->assert_equals(array(3),$this->limit(0,1));
+	}
+
+	public function test_limit_to_last_record()
+	{
+		$this->assert_equals(array(1),$this->limit(2,1));
+	}
+
+	public function test_limit_with_null_offset()
+	{
+		$this->assert_equals(array(3),$this->limit(null,1));
+	}
+
+	public function test_limit_with_nulls()
+	{
+		$this->assert_equals(array(),$this->limit(null,null));
+	}
+
+	public function test_fetch_no_results()
+	{
+		$sth = $this->conn->query('SELECT * FROM authors WHERE author_id=65534');
+		$this->assert_equals(null,$sth->fetch());
+	}
+
+	public function test_tables()
+	{
+		$this->assert_true(count($this->conn->tables()) > 0);
+	}
+
+	public function test_query_column_info()
+	{
+		$this->assert_greater_than(0,count($this->conn->query_column_info("authors")));
+	}
+
+	public function test_query_table_info()
+	{
+		$this->assert_greater_than(0,count($this->conn->query_for_tables()));
+	}
+
+	public function test_query_table_info_must_return_one_field()
+	{
+		$sth = $this->conn->query_for_tables();
+		$this->assert_equals(1,count($sth->fetch()));
+	}
+
+	public function test_transaction_commit()
+	{
+		$original = $this->conn->query_and_fetch_one("select count(*) from authors");
+
+		$this->conn->transaction();
+		$this->conn->query("insert into authors(author_id,name) values(9999,'blahhhhhhhh')");
+		$this->conn->commit();
+
+		$this->assert_equals($original+1,$this->conn->query_and_fetch_one("select count(*) from authors"));
+	}
+
+	public function test_transaction_rollback()
+	{
+		$original = $this->conn->query_and_fetch_one("select count(*) from authors");
+
+		$this->conn->transaction();
+		$this->conn->query("insert into authors(author_id,name) values(9999,'blahhhhhhhh')");
+		$this->conn->rollback();
+
+		$this->assert_equals($original,$this->conn->query_and_fetch_one("select count(*) from authors"));
+	}
+
+	public function test_show_me_a_useful_pdo_exception_message()
+	{
+		try {
+			$this->conn->query('select * from an_invalid_column');
+			$this->fail();
+		} catch (Exception $e) {
+			$this->assert_equals(1,preg_match('/(an_invalid_column)|(exist)/',$e->getMessage()));
+		}
+	}
+
+	public function test_quote_name_does_not_over_quote()
+	{
+		$c = $this->conn;
+		$q = $c::$QUOTE_CHARACTER;
+		$qn = function($s) use ($c) { return $c->quote_name($s); };
+
+		$this->assert_equals("{$q}string", $qn("{$q}string"));
+		$this->assert_equals("string{$q}", $qn("string{$q}"));
+		$this->assert_equals("{$q}string{$q}", $qn("{$q}string{$q}"));
+	}
+
+	public function test_datetime_to_string()
+	{
+		$datetime = '2009-01-01 01:01:01 EST';
+		$this->assert_equals($datetime,$this->conn->datetime_to_string(date_create($datetime)));
+	}
+
+	public function test_date_to_string()
+	{
+		$datetime = '2009-01-01';
+		$this->assert_equals($datetime,$this->conn->date_to_string(date_create($datetime)));
+	}
+}
+?>

+ 130 - 0
hhvm/php-activerecord/test/helpers/DatabaseLoader.php

@@ -0,0 +1,130 @@
+<?php
+class DatabaseLoader
+{
+	private $db;
+	static $instances = array();
+
+	public function __construct($db)
+	{
+		$this->db = $db;
+
+		if (!isset(static::$instances[$db->protocol]))
+			static::$instances[$db->protocol] = 0;
+
+		if (static::$instances[$db->protocol]++ == 0)
+		{
+			// drop and re-create the tables one time only
+			$this->drop_tables();
+			$this->exec_sql_script($db->protocol);
+		}
+	}
+
+	public function reset_table_data()
+	{
+		foreach ($this->get_fixture_tables() as $table)
+		{
+			if ($this->db->protocol == 'oci' && $table == 'rm-bldg')
+				continue;
+
+			$this->db->query('DELETE FROM ' . $this->quote_name($table));
+			$this->load_fixture_data($table);
+		}
+
+		$after_fixtures = $this->db->protocol.'-after-fixtures';
+		try {
+			$this->exec_sql_script($after_fixtures);
+		} catch (Exception $e) {
+			// pass
+		}
+	}
+
+	public function drop_tables()
+	{
+		$tables = $this->db->tables();
+
+		foreach ($this->get_fixture_tables() as $table)
+		{
+			if ($this->db->protocol == 'oci')
+			{
+				$table = strtoupper($table);
+
+				if ($table == 'RM-BLDG')
+					continue;
+			}
+
+			if (in_array($table,$tables))
+				$this->db->query('DROP TABLE ' . $this->quote_name($table));
+
+			if ($this->db->protocol == 'oci')
+			{
+				try {
+					$this->db->query("DROP SEQUENCE {$table}_seq");
+				} catch (ActiveRecord\DatabaseException $e) {
+					// ignore
+				}
+			}
+		}
+	}
+
+	public function exec_sql_script($file)
+	{
+		foreach (explode(';',$this->get_sql($file)) as $sql)
+		{
+			if (trim($sql) != '')
+				$this->db->query($sql);
+		}
+	}
+
+	public function get_fixture_tables()
+	{
+		$tables = array();
+
+		foreach (glob(__DIR__ . '/../fixtures/*.csv') as $file)
+		{
+			$info = pathinfo($file);
+			$tables[] = $info['filename'];
+		}
+
+		return $tables;
+	}
+
+	public function get_sql($file)
+	{
+		$file = __DIR__ . "/../sql/$file.sql";
+
+		if (!file_exists($file))
+			throw new Exception("File not found: $file");
+
+		return file_get_contents($file);
+	}
+
+	public function load_fixture_data($table)
+	{
+		$fp = fopen(__DIR__ . "/../fixtures/$table.csv",'r');
+		$fields = fgetcsv($fp);
+
+		if (!empty($fields))
+		{
+			$markers = join(',',array_fill(0,count($fields),'?'));
+			$table = $this->quote_name($table);
+
+			foreach ($fields as &$name)
+				$name = $this->quote_name(trim($name));
+
+			$fields = join(',',$fields);
+
+			while (($values = fgetcsv($fp)))
+				$this->db->query("INSERT INTO $table($fields) VALUES($markers)",$values);
+		}
+		fclose($fp);
+	}
+
+	public function quote_name($name)
+	{
+		if ($this->db->protocol == 'oci')
+			$name = strtoupper($name);
+
+		return $this->db->quote_name($name);
+	}
+}
+?>

+ 81 - 0
hhvm/php-activerecord/test/helpers/DatabaseTest.php

@@ -0,0 +1,81 @@
+<?php
+require_once 'DatabaseLoader.php';
+
+class DatabaseTest extends SnakeCase_PHPUnit_Framework_TestCase
+{
+	protected $conn;
+	public static $log = false;
+
+	public function set_up($connection_name=null)
+	{
+		ActiveRecord\Table::clear_cache();
+
+		$config = ActiveRecord\Config::instance();
+		$this->original_default_connection = $config->get_default_connection();
+
+		if ($connection_name)
+			$config->set_default_connection($connection_name);
+
+		if ($connection_name == 'sqlite' || $config->get_default_connection() == 'sqlite')
+		{
+			// need to create the db. the adapter specifically does not create it for us.
+			$this->db = substr(ActiveRecord\Config::instance()->get_connection('sqlite'),9);
+			new SQLite3($this->db);
+		}
+
+		$this->connection_name = $connection_name;
+		try {
+			$this->conn = ActiveRecord\ConnectionManager::get_connection($connection_name);
+		} catch (ActiveRecord\DatabaseException $e) {
+			$this->mark_test_skipped($connection_name . ' failed to connect. '.$e->getMessage());
+		}
+
+		$GLOBALS['ACTIVERECORD_LOG'] = false;
+
+		$loader = new DatabaseLoader($this->conn);
+		$loader->reset_table_data();
+
+		if (self::$log)
+			$GLOBALS['ACTIVERECORD_LOG'] = true;
+	}
+
+	public function tear_down()
+	{
+		if ($this->original_default_connection)
+			ActiveRecord\Config::instance()->set_default_connection($this->original_default_connection);
+	}
+
+	public function assert_exception_message_contains($contains, $closure)
+	{
+		$message = "";
+
+		try {
+			$closure();
+		} catch (ActiveRecord\UndefinedPropertyException $e) {
+			$message = $e->getMessage();
+		}
+
+		$this->assert_true(strpos($message,$contains) !== false);
+	}
+
+	/**
+	 * Returns true if $regex matches $actual.
+	 *
+	 * Takes database specific quotes into account by removing them. So, this won't
+	 * work if you have actual quotes in your strings.
+	 */
+	public function assert_sql_has($needle, $haystack)
+	{
+		$needle = str_replace(array('"','`'),'',$needle);
+		$haystack = str_replace(array('"','`'),'',$haystack);
+		return $this->assert_true(strpos($haystack,$needle) !== false);
+	}
+
+	public function assert_sql_doesnt_has($needle, $haystack)
+	{
+		$needle = str_replace(array('"','`'),'',$needle);
+		$haystack = str_replace(array('"','`'),'',$haystack);
+		return $this->assert_false(strpos($haystack,$needle) !== false);
+	}
+}
+?>

Some files were not shown because too many files changed in this diff