Location: PHPKode > projects > MonoQL > monoql/system/Generator.php
<?php
class Generator {

	public static $tables = array();
	public static $classes = array();
	public static $linesOfCode = 0;
	public static $site = null;
	public static $types = array(
		"BIT" => 		array("mssql"=>"BIT","oracle"=>"NUMBER(1)","sqlite"=>"BIT"),
		"TINYINT" => 	array("mssql"=>"TINYINT","oracle"=>"NUMBER(38)","sqlite"=>"TINYINT"),
		"SMALLINT" => 	array("mssql"=>"SMALLINT","oracle"=>"NUMBER(38)","sqlite"=>"SMALLINT"),
		"MEDIUMINT" => 	array("mssql"=>"INT","oracle"=>"NUMBER(38)","sqlite"=>"MEDIUMINT"),
		"INT" => 		array("mssql"=>"INT","oracle"=>"NUMBER(38)","sqlite"=>"INT"),
		"INTEGER" => 	array("mssql"=>"INT","oracle"=>"NUMBER(38)","sqlite"=>"INTEGER"),
		"BIGINT" => 	array("mssql"=>"BIGINT","oracle"=>"NUMBER(38)","sqlite"=>"BIGINT"),
		"REAL" => 		array("mssql"=>"FLOAT(53)","oracle"=>"FLOAT(63)","sqlite"=>"REAL"),
		"DOUBLE" => 	array("mssql"=>"FLOAT(53)","oracle"=>"FLOAT(126)","sqlite"=>"DOUBLE"),
		"FLOAT" => 		array("mssql"=>"FLOAT","oracle"=>"FLOAT","sqlite"=>"FLOAT"),
		"DECIMAL" => 	array("mssql"=>"DECIMAL","oracle"=>"NUMBER","sqlite"=>"DECIMAL"),
		"NUMERIC" => 	array("mssql"=>"NUMERIC","oracle"=>"NUMBER","sqlite"=>"NUMERIC"),
		"DATE" => 		array("mssql"=>"DATETIME","oracle"=>"DATE","sqlite"=>"DATE"),
		"TIME" => 		array("mssql"=>"DATETIME","oracle"=>"DATE","sqlite"=>"TIME"),
		"TIMESTAMP" => 	array("mssql"=>"DATETIME","oracle"=>"TIMESTAMP","sqlite"=>"TIMESTAMP"),
		"DATETIME" => 	array("mssql"=>"DATETIME","oracle"=>"DATE","sqlite"=>"DATETIME"),
		"YEAR" => 		array("mssql"=>"INT","oracle"=>"NUMBER(4)","sqlite"=>"YEAR"),
		"CHAR" => 		array("mssql"=>"CHAR","oracle"=>"CHAR","sqlite"=>"CHAR"),
		"VARCHAR" => 	array("mssql"=>"VARCHAR","oracle"=>"VARCHAR2","sqlite"=>"VARCHAR"),
		"BINARY" => 	array("mssql"=>"BINARY","oracle"=>"RAW","sqlite"=>"BINARY"),
		"VARBINARY" => 	array("mssql"=>"VARBINARY","oracle"=>"BLOB","sqlite"=>"VARBINARY"),
		"TINYBLOB" => 	array("mssql"=>"VARBINARY","oracle"=>"BLOB","sqlite"=>"TINYBLOB"),
		"BLOB" => 		array("mssql"=>"VARBINARY(MAX)","oracle"=>"BLOB","sqlite"=>"BLOB"),
		"MEDIUMBLOB" => array("mssql"=>"VARBINARY(MAX)","oracle"=>"BLOB","sqlite"=>"MEDIUMBLOB"),
		"LONGBLOB" => 	array("mssql"=>"VARBINARY(MAX)","oracle"=>"BLOB","sqlite"=>"LONGBLOB"),
		"TINYTEXT" => 	array("mssql"=>"VARCHAR","oracle"=>"CLOB","sqlite"=>"TINYTEXT"),
		"TEXT" => 		array("mssql"=>"VARCHAR(MAX)","oracle"=>"CLOB","sqlite"=>"TEXT"),
		"MEDIUMTEXT" => array("mssql"=>"VARCHAR(MAX)","oracle"=>"CLOB","sqlite"=>"MEDIUMTEXT"),
		"LONGTEXT" => 	array("mssql"=>"VARCHAR(MAX)","oracle"=>"CLOB","sqlite"=>"LONGTEXT"),
		"ENUM" => 		array("mssql"=>"INT","oracle"=>"NUMBER","sqlite"=>"INT"),
		"SET" => 		array("mssql"=>"INT","oracle"=>"NUMBER","sqlite"=>"INT")
	);
	
	public static function generate($site=null) {
		self::$site = $site;
		self::$tables = array();
		self::$classes = array();
		self::$linesOfCode = 0;
		
		debug("Making temp folder...");
		self::makeFolder(Helix::$path . "/temp");
		
		debug("Importing module schema definition files...");
		self::importModules();
		
		if (isset($site) && strlen($site)>0) {
			debug("Importing site schema definition file for: {$site}...");
			self::importSite($site);
		}
		
		debug("Parsing schema definition files...");
		self::parseTables();
		self::parseTablesAgain();
		
		debug("Writing class files...");
		self::writeClasses();
		
		debug("Updating Helix class paths...");
		self::setHelixClassPaths();
		
		if (isset($site) && strlen($site)>0) {
			debug("Updating site class paths for: {$site}...");
			self::setSiteClassPaths();
		}
		
		debug("Writing MySQL database schema definition...");
		self::writeMySQL();
		
		debug("Writing MySQL log database schema definition...");
		self::writeMySQL(true);
		
		debug("Writing MySQL insert statements...");
		self::writeMySQLInserts();
		
		debug("Generator Done: " . count(self::$tables) . " tables generated");
	}
	
	public static function setHelixClassPaths($folder=null) {
		$files = array();
		$paths = isset($folder) ? glob("{$folder}/*") : glob(Helix::$path . "/*");
		
		foreach ($paths as $path) {
			if (in_array(strrchr($path, "/"), array("/External", "/Resources", "/Applications"))) {continue;}
			if (is_file($path) && strrchr($path,".")===".php") {
				$files[basename($path)] = preg_replace('/^' . preg_quote(Helix::$path,"/") . '[\\\\\\/]*(.*)$/i','$1',$path);
			} else if (is_dir($path)) {
				$files = array_merge($files, self::setHelixClassPaths($path));
			}
		}
		ksort($files);
		
		$lines = array("{");
		foreach ($files as $file=>$path) {
			$class = basename($file, ".php");
			$lines[] = TAB . json_encode($class) . ":\"" . str_replace("\\", "/", $path) . "\",";
		}
		$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
		$lines[] = "}";
		$text = implode(NL, $lines);
		file_put_contents(Helix::$path . "/config/helix.classes.json", $text);
		
		return $files;
	}
	
	public static function setSiteClassPaths($folder=null) {
		$site = self::$site;
		$files = array();
		$baseFolder = dirname(realpath("."));
		$paths = isset($folder) ? glob("{$folder}/*") : glob("{$baseFolder}/{$site}/library/*");
		
		foreach ($paths as $path) {
			if (strrchr($path, "/")==="/External" || strrchr($path, "/")==="/Resources") {continue;}
			if (is_file($path) && strrchr($path,".")===".php") {
				$files[basename($path)] = preg_replace('/^' . preg_quote("{$baseFolder}/{$site}/","/") . '[\\\\\\/]*(.*)$/i','$1',$path);
			} else if (is_dir($path)) {
				$files = array_merge($files, self::setSiteClassPaths($path));
			}
		}
		ksort($files);
		
		$lines = array("{");
		foreach ($files as $file=>$path) {
			$class = basename($file, ".php");
			$lines[] = TAB . json_encode($class) . ":\"" . str_replace("\\", "/", $path) . "\",";
		}
		$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
		$lines[] = "}";
		$text = implode(NL, $lines);
		file_put_contents("{$baseFolder}/{$site}/library/Config/{$site}.classes.json", $text);
		
		return $files;
	}
	
	public static function importJSON($path, $module=null, $site=null) {
		$schema = json_decode(file_get_contents($path), true);
		$data = json_decode(file_get_contents(str_replace("schema.json","data.json",$path)), true);
		foreach ($schema["tables"] as $name=>$table) {
			$table["module"] = $module;
			$table["site"] = $site;
			if (array_key_exists("tables", $data) && array_key_exists($name, $data["tables"])) {
				$table["data"] = $data["tables"][$name]["data"];
			}
			self::$tables[$name] = $table;
		}
	}
	
	public static function makeFolder($path) {
		if (!file_exists($path)) {
			mkdir($path, 0755, true);
		}
	}
	
	public static function emptyFolder($path, $deleteFolders=false) {
		if (is_dir($path)) {
			foreach (glob($path . "/*") as $file) {
				if (is_dir($file)) {
					self::emptyFolder($file, $deleteFolders);
					if ($deleteFolders) {
						rmdir($file);
					}
				} else {
					unlink($file);
				}
			}
		}
	}
	
	public static function importModules() {
		foreach (glob(Helix::$path . "/modules/*/Config/*.schema.json") as $path) {
			$module = basename(dirname(dirname($path)));
			self::importJSON($path, $module, null);
			self::emptyFolder(dirname(dirname($path)) . "/Generated");
		}
	}
	
	public static function importSite($site) {
		$path = dirname(realpath(".")) . "/{$site}/library/Config/{$site}.schema.json";
		self::importJSON($path, null, $site);
		self::emptyFolder(dirname(dirname($path)) . "/Generated");
	}
	
	public static function parseTables() {
		foreach (self::$tables as $tablename=>&$table) {
			$table["name"] = $tablename;
			$table["parts"] = explode("_",$tablename);
			$table["class"] = str_replace(" ","",ucwords(str_replace("_"," ",strtolower($tablename))));
			$table["has_type"] = array_key_exists("{$tablename}_type",self::$tables);
			$table["is_type"] = strrchr($tablename,"_")==="_type";
			$table["type"] = self::getType($table);
			$table["is_ordered"] = array_key_exists("order",$table["columns"]);
			$table["is_self_referential"] = count($table["parts"])===2 && $table["parts"][0]===$table["parts"][1];
			$table["is_child"] = array_key_exists("_id",$table["columns"]);
			$table["is_parent"] = false;
			$table["is_column_extension"] = count($table["columns"])===6 && count(array_diff(array_keys($table["columns"]), array("id","value","updated_by_id","mdate","cdate","deleted")))===0;
			if ($table["is_self_referential"]) {
				self::$tables[$table["parts"][0]]["is_self_linked"] = true;
			}
			$table["is_self_linked"] = isset($table["is_self_linked"]) ? $table["is_self_linked"] : false;
			foreach ($table["columns"] as $colname=>&$column) {
				$column["name"] = $colname;
				$column["table"] = $tablename;
				$column["php_type"] = self::getPHPType($column["type"]);
				if ($table["is_self_referential"] && preg_match('/^(child|parent)_' . $table["parts"][0] . '_id$/', $colname)) {
					$column["property"] = camel(str_replace("_{$table["parts"][0]}_", "_", $colname));
				} else {
					$column["property"] = camel($colname);
				}
				
				if (val($column,"key")==="primary") {
					$column["is_primary"] = true;
					$table["primary_keys"][] = $colname;
				} else {
					$column["is_primary"] = false;
				}
				
				if (val($column,"key")==="unique") {
					$column["is_unique"] = true;
					$table["unique_keys"][] = $colname;
				} else if (is_array(val($column,"key")) && $column["key"][0]==="unique") {
					$column["is_unique"] = true;
					$table["unique_keys"][] = array($colname, $column["key"][1]);
				} else {
					$column["is_unique"] = false;
				}
				$column["auto"] = val($column,"auto")===true;
				$column["default"] = val($column,"default");
				if (array_key_exists("references",$column)) {
					$reftable = $column["references"][0];
					$refcol = $column["references"][1];
					$niceref = $table["is_self_referential"] && preg_match('/^(parent|child).*$/i', $colname) ? preg_replace('/^(parent|child).*$/i','$1',$colname) : $reftable;
					$table["references"][$niceref] = array("local"=>$colname, "foreign"=>$refcol);
					self::$tables[$reftable]["referenced_by"][$tablename] = array("local"=>$refcol, "foreign"=>$colname);
					if ($table["type"]==="RELATIONSHIP" && strrchr($reftable,"_")!=="_type") {
						$table["related"][] = array("table"=>$reftable,"column"=>$refcol,"reltable"=>$tablename,"relcol"=>$colname);
					}
					if ($table["type"]==="STANDARD" && val($column,"key")==="primary") {
						$table["parent"] = $reftable; 
						self::$tables[$reftable]["is_parent"] = true;
						self::$tables[$reftable]["children"][] = $tablename;
						foreach (self::$tables[$reftable]["children"] as $child) {
							$other = array_diff(self::$tables[$reftable]["children"],array($child));
							if (count($other)>0) {
								self::$tables[$child]["siblings"] = $other;
							}
						}
					}
				}
			}
			if ($table["type"]==="RELATIONSHIP") {
				self::$tables[$table["related"][0]["table"]]["referenced_by"][$tablename]["linked_table"] = $table["related"][1]["table"];
				self::$tables[$table["related"][1]["table"]]["referenced_by"][$tablename]["linked_table"] = $table["related"][0]["table"];
				self::$tables[$table["related"][0]["table"]]["linked"][$table["related"][1]["table"]] = array(
					"local"=>array($table["related"][0]["table"], $table["related"][0]["column"]),
					"linked"=>array($table["related"][1]["table"], $table["related"][1]["column"]),
					"relationship"=>array("table"=>$table["related"][0]["reltable"], "local"=>$table["related"][0]["relcol"], "linked"=>$table["related"][1]["relcol"])
				);
				self::$tables[$table["related"][1]["table"]]["linked"][$table["related"][0]["table"]] = array(
					"local"=>array($table["related"][1]["table"], $table["related"][1]["column"]),
					"linked"=>array($table["related"][0]["table"], $table["related"][0]["column"]),
					"relationship"=>array("table"=>$table["related"][1]["reltable"], "local"=>$table["related"][1]["relcol"], "linked"=>$table["related"][0]["relcol"])
				);
			}
			$table["collapsed_columns"] = $table["columns"];
			$table["collapsed_primary_keys"] = $table["primary_keys"];
			$table["collapsed_unique_keys"] = isset($table["unique_keys"]) ? $table["unique_keys"] : array();
			$table["primary_key"] = implode(",",$table["primary_keys"]);
			$table["id_column"] = $table["type"]==="RELATIONSHIP" ? "id" : $table["primary_key"];
		}
	}
	
	public static function parseTablesAgain() {
		foreach (self::$tables as &$table) {
			if ($table["type"]==="RELATIONSHIP") {
				foreach ($table["references"] as $reftable=>$refdata) {
					if ($table["is_self_referential"] && ($reftable==="child" || $reftable==="parent")) {
						$reftablename = $table["parts"][0];
						$refParam = "\$" . $reftable . "Id";
					} else {
						$ref = self::$tables[$reftable];
						$reftablename = $reftable;
						if ($ref["type"]!=="STANDARD" && $ref["type"]!=="RELATIONSHIP") {continue;}
						$refParam = "\$" . lcfirst(self::$tables[$reftable]["class"]) . "Id";
					}
					$params[] = array("table"=>$reftablename, "column"=>$refdata["local"], "param"=>$refParam);
				}
				ksort($params);
				$table["params"] = $params;
				unset($params);
			}
			
			if ($table["type"]==="TYPE" || $table["type"]==="RELATIONSHIP_TYPE") {
				$table["is_lookup"] = true;
			} else if (isset($table["referenced_by"])) {
				$isReferencedByPrimaryKey = false;
				foreach ($table["referenced_by"] as $refbytablename=>$refbydata) {
					$refbytable = self::$tables[$refbytablename];
					$isReferencedByPrimaryKey = $isReferencedByPrimaryKey || in_array($refbydata["foreign"], $refbytable["primary_keys"]);
				}
				$table["is_lookup"] = !$isReferencedByPrimaryKey;
			} else {
				$table["is_lookup"] = false;
			}
			
			if ($table["is_lookup"]) {
				if (isset($table["data"])) {
					if (isset($table["data"]["values"])) {
						if (count($table["columns"])===7 && array_key_exists("name", $table["columns"]) && array_key_exists("description", $table["columns"])) {
							if (count($table["data"]["values"])===0) {
								$table["data"]["columns"] = array("id", "name", "description");
								$table["data"]["values"][0] = array("1", "default", "Default");
							}
						}
					} else {
						if (count($table["columns"])===7 && array_key_exists("name", $table["columns"]) && array_key_exists("description", $table["columns"])) {
							$table["data"]["columns"] = array("id", "name", "description");
							$table["data"]["values"][0] = array("1", "default", "Default");
						}
					}
					foreach ($table["data"]["values"] as $i=>&$values) {
						if ($i>0) {
							$values[0] = $i+1;
						}
					}
				} else {
					if (count($table["columns"])===7 && array_key_exists("name", $table["columns"]) && array_key_exists("description", $table["columns"])) {
						$table["data"]["columns"] = array("id", "name", "description");
						$table["data"]["values"][0] = array("1", "default", "Default");
					}
				}
			}
			
			$child = $table;
			while ($child["is_child"]) {
				$parent = self::$tables[$child["parent"]];
				$columns = array();
				foreach ($parent["columns"] as $colname=>&$column) {
					if (!array_key_exists($colname,$child["columns"])) {
						$columns[$colname] = $column;
					}
				}
				$table["collapsed_primary_keys"] = array_merge($parent["primary_keys"], $table["collapsed_primary_keys"]);
				if (isset($parent["unique_keys"])) {
					$table["collapsed_unique_keys"] = array_merge($parent["unique_keys"], $table["collapsed_unique_keys"]);
				}
				$table["collapsed_columns"] = array_merge($columns, $table["collapsed_columns"]);
				$child = $parent;
			}
			
			$niceColumns = array();
			$insertColumns = $table["type"]==="RELATIONSHIP" ? array() : array($table["primary_key"]=>$table["columns"][$table["primary_key"]]);
			$niceCollapsedColumns = array();
			foreach ($table["collapsed_columns"] as $colname=>$column) {
				if ($colname==="_id" || ($table["type"]!=="RELATIONSHIP" && $colname!=="id" && in_array($colname,$table["collapsed_primary_keys"]))) {continue;}
				if ($colname==="id" || isset($table["columns"][$colname])) {
					$niceColumns[$colname] = $column;
				}
				if ($colname!=="_id" && isset($table["columns"][$colname])) {
					$insertColumns[$colname] = $column;
				}
				$niceCollapsedColumns[$colname] = $column;
			}
			$table["nice_columns"] = $niceColumns;
			$table["insert_columns"] = $insertColumns;
			$table["nice_collapsed_columns"] = $niceCollapsedColumns;
		
			if ($table["type"]==="RELATIONSHIP") {
				$table["folder"] = "Relationships";
			} else if ($table["type"]==="RELATIONSHIP_TYPE") {
				$table["folder"] = "Relationships/Types";
			} else if ($table["type"]==="TYPE") {
				$table["folder"] = "Objects/Types";
			} else if ($table["is_lookup"]) {
				$table["folder"] = "Objects/Lookups";
			} else if ($table["is_column_extension"]) {
				$table["folder"] = "Objects/ColumnExtensions";
			} else {
				$table["folder"] = "Objects";
			}
			
			ksort($table);
		}
		
		ksort(self::$tables);
		file_put_contents(Helix::$path . "/temp/generator.txt", print_r(self::$tables, true));
	}
	
	public static function writeClasses() {
		$i = 0;
		foreach (self::$tables as $tablename=>$table) {
			debug("[" . String::lpad(++$i, 3, 0) . "] Writing classes for: {$tablename}" . (isset($table["linked"]) ? " [" . implode(", ", array_keys($table["linked"])) . "]" : ""));
			$moduleFolder = isset($table["module"]) ? Helix::$path . "/modules/{$table["module"]}" : dirname(realpath(".")) . "/{$table["site"]}/library";
			$tableFolder = "{$moduleFolder}/{$table["folder"]}";
			$generatedFolder = "{$moduleFolder}/Generated/{$table["folder"]}";
			$stubFolder = "{$generatedFolder}/Stubs";
			self::makeFolder($tableFolder);
			self::makeFolder($stubFolder);
			$text = implode(NL, array(
			"<?php",
			"/**",
			" * DO NOT EDIT -- This is an auto-generated class from the Helix Class Generator",
			" * ",
			" * This class represents the {$table["name"]} table in the Helix database schema.",
			" * Use this class to select, insert, update and delete data in the {$table["name"]}",
			" * table, as well as access related data in other tables.",
			" * ",
			" * If you need to extend the functionality of this class, code should be placed in a",
			" * class called {$table["class"]}Extension, and should extend the {$table["class"]}Table",
			" * class.  The custom code file should be in the helix/modules/{$table["module"]} folder",
			" * and should be called {$table["class"]}Extension.php",
			" * ",
			" * " . self::inheritanceBreadCrumbs($table),
			" */",
			"class {$table["class"]}Table extends " . ($table["is_child"] ? self::$tables[$table["parent"]]["class"] : "Object") . " {",
			"	public \$logSequence;",
			"	public \$fdate;",
			"	public \$tdate;",
			(!$table["is_child"] ? implode(NL, array(
			"",
			"	protected \$_cache = array();",
			"	protected \$_initial = array();",
			"	protected \$_snapshot;",
			""
			)) :
			""
			),
				self::defineProperties($table),
			"",
			"	public function __construct(" . self::listConstructorArgs($table, true) . ") {",
			"		\$db = Database::getInstance();",
					self::setPropertyDefaults($table),
			"",
					self::setParentJoinCondition($table),
			"",
			"		if (isset(\$condition)) {",
						self::setSelectStatement($table),
			"",
			"			\$db->query(\$query);",
			"",
			"			if (\$db->getRecord() && \$db->getNumRows()===1) {",
							self::setProperties($table),
			"			} else {",
			"				\$this->id = null;",
			"			}",
			"		}",
			"",
					self::setInitialValueArray($table),
			"	}",
			"",
			"	public static function snapshot(\$date, " . self::listConstructorArgs($table, true, true) . ") {",
			"		\$object = new {$table["class"]}();",
			"		\$object->_snapshot = \$date;",
			"		\$db = Database::getInstance();",
			"		\$db->changeDatabase(\$db->getDatabase() . \"_log\");",
			"",
					self::setParentJoinCondition($table),
			"",
			"		if (isset(\$condition)) {",
						self::setSnapshotCondition($table),
						self::setSelectStatement($table, null, true),
			"",
			"			\$db->query(\$query);",
			"",
			"			if (\$db->getRecord() && \$db->getNumRows()===1) {",
			"				\$object->logSequence = \$db->record[\"log_sequence\"];",
			"				\$object->fdate = new Date(\$db->record[\"fdate\"]);",
			"				\$object->tdate = new Date(\$db->record[\"tdate\"]);",
							self::setProperties($table, "object"),
			"			} else {",
			"				\$object->id = null;",
			"			}",
			"		}",
			"		",
			"		return \$object;",
			"	}",
			"",
			"	public function __call(\$method, \$arguments) {",
			"		if (preg_match('/^set(.*)$/', \$method, \$matches)) {",
			"			\$property = lcfirst(\$matches[1]);",
			"			\$this->{\$property} = \$arguments[0];",
			"		}",
			"		return \$this;",
			"	}",
			"",
			"	public function __get(\$property) {",
			"		if (method_exists(\$this, \"get{\$property}\")) {",
			"			return \$this->{\"get{\$property}\"}();",
			"		} else if (strstr(\$property, \"_\")) {",
			"			list(\$type, \$method) = explode(\"_\", \$property, 2);",
			"			return method_exists(\$this, \"get{\$method}\") ? \$this->{\"get{\$method}\"}(\$type) : null;",
			"		} else {",
			"			return null;",
			"		}",
			"	}",
			"",
			"	public function __set(\$property, \$value) {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		if (method_exists(\$this, \"set{\$property}\")) {",
			"			\$this->{\"set{\$property}\"}(\$value);",
			"		} else if (strstr(\$property, \"_\")) {",
			"			list(\$type, \$method) = explode(\"_\", \$property, 2);",
			"			if (method_exists(\$this, \"set{\$method}\")) {",
			"				\$this->{\"set{\$method}\"}(\$value, \$type);",
			"			}",
			"		}",
			"		return \$this;",
			"	}",
			"",
			"	public function isDirty() {",
			"		\$isDirty = false;",
			"",
					self::checkIsDirty($table),
			"",
			"		return \$isDirty;",
			"	}",
			"",
			"	public function save() {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		\$status = \$this->id>0 ? \$this->update() : \$this->insert();",
			"",
			"		foreach (\$this->_cache as \$class=>\$list) {",
			"			foreach (\$list as \$type=>\$object) {",
			"				\$object->save();",
			"				\$this->{\"add\" . \$object->getClass()}(\$object, \$type);",
			"			}",
			"			unset(\$this->_cache[\$class]);",
			"		}",
			"",
			"		return \$status;",
			"	}",
			"",
			"	public function insert(" . ($table["is_child"] ? "\$insertParent=true" : "") . ") {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		global \$session, \$config;",
			"",
			"		\$db = Database::getInstance();",
					self::setInsertStatement($table),
			"",
			"		if (\$config[\"enable_database_log\"]) {",
			"			\$this->log();",
			"		}",
			"",
			"		return \$status;",
			"	}",
			"",
			"	public function update() {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		global \$session, \$config;",
			"",
			($table["is_child"] ? implode(NL, array(
			"		parent::update();",
			"		if (\$this->isDirty()) {"
			)) :
			"		if (\$this->isDirty()) {"
			),
			"			\$db = Database::getInstance();",
						self::setUpdateStatement($table),
			"			\$status = \$db->query(\$query);",
			"",
			"			if (\$config[\"enable_database_log\"]) {",
			"				\$this->log();",
			"			}",
			"",
			"			return \$status;",
			"		} else {",
			"			return false;",
			"		}",
			"	}",
			"",
			"	private function log() {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		\$db = Database::getInstance();",
			"		\$database = \$db->getDatabase();",
			"		\$log = \"{\$database}_log\";",
			"		\$db->changeDatabase(\$log);",
					self::setLogStatements($table),
			"		\$db->changeDatabase(\$database);",
			"		return \$status;",
			"	}",
			($table["is_child"] && !$table["is_self_linked"]? implode(NL, array(
			"",
			"	public function getParent() {",
			"		\$this->get" . self::$tables[$table["parent"]]["class"] . "();",
			"	}",
			"",
			"	public function addSibling(\$object) {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		if (method_exists(\$object, \"getParent\") && is_null(\$object->id) && \$object->getParent()->getClass()===\$this->getParent()->getClass()) {",
			"			\$object->id = \$this->id;",
			"			return \$object->insert(false);",
			"		} else {",
			"			return false;",
			"		}",
			"	}",
			""
			)) : 
			""
			),
			"	public function delete() {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		\$this->deleted = true;",
			"		\$status = \$this->update();",
			"		return \$status;",
			"	}",
			"",
			"	public function unDelete() {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		\$this->deleted = false;",
			"		\$status = \$this->update();",
			"		return \$status;",
			"	}",
			"",
			"	public function purge() {",
			"		if (isset(\$this->_snapshot)) {return false;}",
			"		\$db = Database::getInstance();",
			"		\$query = \"DELETE FROM {$tablename} WHERE {$table["id_column"]}={\$db->queryValue(\$this->id)}\";",
			"		\$status = \$db->query(\$query);",
			"		return \$status;",
			"	}",
			"",
			"	public static function deleteAll(" . ($table["type"]==="RELATIONSHIP" ? "{$table["params"][0]["param"]}=null, {$table["params"][1]["param"]}=null, \$type=null" : "\$where=null") . ") {",
			"		\$db = Database::getInstance();",
			($table["type"]==="RELATIONSHIP" ? implode(NL, array(
			"		\$conditions = array();",
			"		if (isset({$table["params"][0]["param"]})) {",
			"			\$conditions[] = \"{\$db->le}{$table["params"][0]["column"]}{\$db->re}={\$db->queryValue({$table["params"][0]["param"]})}\";",
			"		}",
			"		if (isset({$table["params"][1]["param"]})) {",
			"			\$conditions[] = \"{\$db->le}{$table["params"][1]["column"]}{\$db->re}={\$db->queryValue({$table["params"][1]["param"]})}\";",
			"		}",
			"		if (isset(\$type)) {",
			"			\$conditions[] = \"{\$db->le}{$table["name"]}_type_id{\$db->re}=\" . \$db->queryValue(self::typeId(\$type));",
			"		}",
			"		\$condition = count(\$conditions)===0 ? \"\" : \" WHERE \" . implode(\" AND \", \$conditions);"
			)) : 
			"		\$condition = isset(\$where) ? \"WHERE {\$where}\" : \"\";"
			),
			"		\$query = \"UPDATE {$tablename} SET deleted=1 {\$condition}\";",
			"		\$status = \$db->query(\$query);",
			"		return \$status;",
			"	}",
			"",
			"	public static function select(\$columns, \$order=null, \$where=null, \$limit=null, \$offset=0) {",
			"		\$db = Database::getInstance();",
			"",
			"		\$records = array();",
			"		\$columns = is_array(\$columns) ? \$columns : explode(\",\", \$columns);",
			"",
					self::setGetStatement($table),
			"",
			"		\$db->query(\$query);",
			"",
			"		if (count(\$columns)>1) {",
			"			while (\$db->getRecord()) {",
			"				\$records[] = \$db->record;",
			"			}",
			"		} else {",
			"			while (\$db->getRecord(false)) {",
			"				\$records[] = \$db->record[0];",
			"			}",
			"		}",
			"",
			"		return new Collection(\$records);",
			"	}",
			"",
			"	public static function ids(\$order=null, \$where=null, \$limit=null, \$offset=0) {",
			"		return {$table["class"]}::select(\"{$table["id_column"]}\", \$order, \$where, \$limit, \$offset);",
			"	}",
			"",
			"	public static function objects(\$order=null, \$where=null, \$limit=null, \$offset=0) {",
			"		\$db = Database::getInstance();",
			"		\$objects = array();",
			"		foreach ({$table["class"]}::select(\"" . self::listConstructorQueryColumns($table) . "\", \$order, \$where, \$limit, \$offset) as \$record) {",
			"			\$object = new {$table["class"]}();",
						self::setProperties($table, "object", "record"),
			"			\$objects[] = \$object;",
			"		}",
			"		return new Collection(\$objects);",
			"	}",
			"",
			"	public static function search(\$query=null, \$order=null, \$where=null, \$limit=null, \$offset=0) {",
			"		\$keywords = array();",
			"		\$clauses = array();",
			"",
			"		preg_match_all('/\"([^\"]+)\"/i', \$query, \$matches, PREG_SET_ORDER);",
			"		foreach (\$matches as \$match) {",
			"			\$keywords[] = \$match[1];",
			"		}",
			"",
			"		\$query = preg_replace('/\"[^\"]+\"/i', '', \$query);",
			"		foreach (preg_split('/ +/i',\$query) as \$keyword) {",
			"			\$keywords[] = \$keyword;",
			"		}",
			"",
			"		foreach (\$keywords as \$keyword) {",
			"			\$clauses[] = \"" . (is_null(self::searchClause($table)) ? "id LIKE '%{\$keyword}%'" : self::searchClause($table)) . "\";",
			"		}",
			"",
			"		\$search = implode(\" AND \", \$clauses);",
			"		\$where = isset(\$where) ? \"{\$where} AND ({\$search})\" : \"({\$search})\";",
			"		return {$table["class"]}::objects(\$order, \$where, \$limit, \$offset);",
			"	}",
			"",
			"	public function __toString() {",
			"		return " . ($table["is_column_extension"] ? "\$this->value" : "\"{$table["class"]} Object [\" . alt(\$this->id, \"null\") . \"]\"") . ";",
			"	}",
			"",
			"	public function string() {",
			"		return \$this->__toString();",
			"	}",
				self::typeMethods($table),
				self::referencedByObjects($table),
				self::referencedObjects($table),
				self::linkedObjects($table),
				($table["is_self_linked"] ? self::selfLinkedObjects($table) : ""),
			"}",
			"?>"
			));
			file_put_contents("{$generatedFolder}/{$table["class"]}Table.php", $text);
			self::$classes["{$table["class"]}Table"] = "{$generatedFolder}/{$table["class"]}Table.php";
			self::$linesOfCode += count(explode(NL, $text));
			
			$text = implode(NL, array(
			"<?php",
			"/**",
			" * DO NOT EDIT -- This is an auto-generated class from the Helix Class Generator",
			" * ",
			" * This class should never contain any code.",
			" * ",
			" * " . self::inheritanceBreadCrumbs($table),
			" */",
			"class {$table["class"]} extends {$table["class"]}Extension {}",
			"?>"
			));
			file_put_contents("{$stubFolder}/{$table["class"]}.php", $text);
			self::$classes[$table["class"]] = "{$stubFolder}/{$table["class"]}.php";
			self::$linesOfCode += count(explode(NL, $text));
			
			$text = implode(NL, array(
			"<?php",
			"/**",
			" * DO NOT EDIT -- This is an auto-generated class from the Helix Class Generator",
			" * ",
			" * This class should never contain any code.",
			" * ",
			" * " . self::inheritanceBreadCrumbs($table),
			" */",
			"class {$table["class"]}Relationships extends {$table["class"]}Table {}",
			"?>"
			));
			file_put_contents("{$stubFolder}/{$table["class"]}Relationships.php", $text);
			self::$classes["{$table["class"]}Relationships"] = "{$stubFolder}/{$table["class"]}Relationships.php";
			self::$linesOfCode += count(explode(NL, $text));

			if (file_exists("{$tableFolder}/{$table["class"]}Extension.php")) {
				$text = file_get_contents("{$tableFolder}/{$table["class"]}Extension.php");
			} else {
				$text = implode(NL, array(
				"<?php",
				"/**",
				" * This is an extension of the {$table["class"]}Relationships class in the Helix Class Library",
				" * ",
				" * Add methods and/or properties to this class to extend the functionality of the",
				" * {$table["class"]} class family.  Changes to this class will affect all sites that use",
				" * this installation of the Helix Class Library.",
				" * ",
				" * If you need to customize this class for a single site, custom code should be placed",
				" * in a class called {$table["class"]}, and should extend the {$table["class"]}Extension class.",
				" * The custom code file should be in the site folder called: library/{$table["class"]}.php",
				" * ",
				" * " . self::inheritanceBreadCrumbs($table),
				" */",
				"class {$table["class"]}Extension extends {$table["class"]}Relationships {",
				"",
				"}",
				"?>"
				));
			}
			file_put_contents("{$tableFolder}/{$table["class"]}Extension.php", $text);
			self::$classes["{$table["class"]}Extension"] = "{$tableFolder}/{$table["class"]}Extension.php";
			self::$linesOfCode += count(explode(NL, $text));
		}
	}
	
	private static function linkedObjects($table) {
		$classCode = null;
		$siteClassCode = null;
		
		if (isset($table["linked"])) {
			ksort($table["linked"]);
			foreach ($table["linked"] as $linktable=>$linked) {
				$lines = array();
				if ($table["name"]===$linktable) {continue;}
				$link = self::$tables[$linktable];
				$rel = self::$tables[$linked["relationship"]["table"]];
				$param = "\$" . lcfirst($link["class"]);
				$lines[] = "";
				$lines[] = TAB . "public function set{$link["class"]}({$param}=null, \$type=\"default\") {";
				$lines[] = TAB . TAB . "if (isset(\$this->_snapshot)) {return false;}";
				$lines[] = TAB . TAB . "\$this->remove{$link["class"]}List(\$type);";
				if ($link["is_column_extension"]) {
					$lines[] = TAB . TAB . "{$param} = is_object({$param}) || is_int({$param}) ? {$param} : \$this->get{$link["class"]}(\$type, true)->setValue({$param});";
				}
				$lines[] = TAB . TAB . "\$this->add{$link["class"]}({$param}, \$type);";
				$lines[] = TAB . TAB . "return \$this;";
				$lines[] = TAB . "}";
				
				$lines[] = TAB . "public function remove{$link["class"]}({$param}, \$type=\"default\", \$deleteObject=true) {";
				$lines[] = TAB . TAB . "if (isset(\$this->_snapshot)) {return false;}";
				$lines[] = TAB . TAB . "\$list = is_array({$param}) ? {$param} : array({$param});";
				$lines[] = TAB . TAB . "foreach (\$list as \$item) {";
				$lines[] = TAB . TAB . "	\$id = is_object(\$item) ? \$item->id : \$item;";
				$lines[] = TAB . TAB . "	\$relationship = \$this->get{$rel["class"]}(\$id, \$type);";
				$lines[] = TAB . TAB . "	if (\$deleteObject) {";
				$lines[] = TAB . TAB . "		\$relationship->get{$link["class"]}()->delete();";
				$lines[] = TAB . TAB . "	}";
				$lines[] = TAB . TAB . "	\$relationship->delete();";
				$lines[] = TAB . TAB . "}";
				$lines[] = TAB . TAB . "return \$this;";
				$lines[] = TAB . "}";
				
				$lines[] = TAB . "public function remove{$link["class"]}List(\$type=null) {";
				$lines[] = TAB . TAB . "if (isset(\$this->_snapshot)) {return false;}";
				$lines[] = TAB . TAB . "if (\$this->id>0) {";
				$lines[] = TAB . TAB . "	return {$rel["class"]}::deleteAll(" . ($rel["params"][0]["table"]===$table["name"] ? "\$this->id" : "null") . ", " . ($rel["params"][1]["table"]===$table["name"] ? "\$this->id" : "null") . ", \$type);";
				$lines[] = TAB . TAB . "}";
				$lines[] = TAB . "}";
				
				$lines[] = TAB . "public function add{$link["class"]}({$param}=null, \$type=\"default\") {";
				$lines[] = TAB . TAB . "if (isset(\$this->_snapshot)) {return false;}";
				$lines[] = TAB . TAB . "if (isset({$param})) {";
				$lines[] = TAB . TAB . "	if (!\$this->id) {";
				$lines[] = TAB . TAB . "		\$this->save();";
				$lines[] = TAB . TAB . "	}";
				$lines[] = TAB . TAB . "	\$list = is_array({$param}) ? {$param} : array({$param});";
				$lines[] = TAB . TAB . "	\$order = 0;";
				$lines[] = TAB . TAB . "	foreach (\$list as \$item) {";
				$lines[] = TAB . TAB . "		if (is_object(\$item) && !\$item->id) {";
				$lines[] = TAB . TAB . "			\$item->save();";
				$lines[] = TAB . TAB . "		}";
				$lines[] = TAB . TAB . "		\$id = is_object(\$item) ? \$item->id : \$item;";
				$lines[] = TAB . TAB . "		\$relationship = \$this->get{$rel["class"]}(\$id, \$type);";
				if ($rel["is_ordered"]) {
					$lines[] = TAB . TAB . "		\$relationship->order = ++\$order;";
				}
				$lines[] = TAB . TAB . "		\$relationship->deleted = false;";
				$lines[] = TAB . TAB . "		\$relationship->save();";
				$lines[] = TAB . TAB . "	}";
				$lines[] = TAB . TAB . "}";
				$lines[] = TAB . TAB . "return \$this;";
				$lines[] = TAB . "}";
				
				$lines[] = TAB . "public function get{$link["class"]}(\$type=\"default\"" . ($link["is_column_extension"] ? ", \$getAsObject=false" : "") . ") {";
				$lines[] = TAB . TAB . "if (isset(\$this->_cache[\"{$link["class"]}\"]) && isset(\$this->_cache[\"{$link["class"]}\"][\$type])) {";
				$lines[] = TAB . TAB . "	{$param} = \$this->_cache[\"{$link["class"]}\"][\$type];";
				$lines[] = TAB . TAB . "} else {";
				$lines[] = TAB . TAB . "	{$param} = new {$link["class"]}(\$this->get{$link["class"]}Id(\$type));";
				$lines[] = TAB . TAB . "}";
				$lines[] = TAB . TAB . "\$this->_cache[\"{$link["class"]}\"][\$type] = {$param};";
				if ($link["is_column_extension"]) {
					$lines[] = TAB . TAB . "return \$getAsObject ? \$this->_cache[\"{$link["class"]}\"][\$type] : \$this->_cache[\"{$link["class"]}\"][\$type]->value;";
				} else {
					$lines[] = TAB . TAB . "return \$this->_cache[\"{$link["class"]}\"][\$type];";
				}
				$lines[] = TAB . "}";
				
				$lines[] = TAB . "public function get{$link["class"]}List(\$type=null, \$order=null, \$where=null, \$limit=null, \$offset=0, \$primary=false) {";
				$lines[] = TAB . TAB . "\$db = Database::getInstance();";
				$lines[] = TAB . TAB . "\$ids = \$this->get{$link["class"]}Ids(\$type, \$order, \$where, \$limit, \$offset, \$primary);";
				$lines[] = TAB . TAB . "\$list = \$ids->count()===0 ? new Collection() : {$link["class"]}::objects(\$order, \"{\$db->le}{$link["name"]}{\$db->le}.{\$db->re}{$link["id_column"]}{\$db->re} IN (\" . \$ids->join(\",\") . \")\");";
				$lines[] = TAB . TAB . "return \$list;";
				$lines[] = TAB . "}";
				
				$lines[] = TAB . "public function get{$link["class"]}Id(\$type=\"default\") {";
				$lines[] = TAB . TAB . "return \$this->get{$link["class"]}Ids(\$type)->get(0);";
				$lines[] = TAB . "}";
				
				$lines[] = TAB . "public function get{$link["class"]}Ids(\$type=null, \$order=null, \$where=null, \$limit=null, \$offset=0, \$primary=false) {";
				$lines[] = TAB . TAB . "\$db = Database::getInstance();";
				$lines[] = TAB . TAB . "\$ids = array();";
				$lines[] = TAB . TAB . "";
				$lines[] = TAB . TAB . "if (isset(\$this->_snapshot)) {";
				$lines[] = TAB . TAB . TAB . "\$date = \$this->_snapshot;";
				$lines[] = TAB . TAB . TAB . "\$condition = \" {\$db->le}{$linktable}{\$db->re}.{\$db->le}tdate{\$db->re} IS NOT NULL AND {\$db->le}{$linktable}{\$db->re}.{\$db->le}fdate{\$db->re}<={\$db->queryValue(\$date)} AND {\$db->queryValue(\$date)}<={\$db->le}{$linktable}{\$db->re}.{\$db->le}tdate{\$db->re} \";";
				$lines[] = TAB . TAB . TAB . "\$where = isset(\$where) ? \"{\$where} AND ({\$condition})\" : \$condition;";
				$lines[] = TAB . TAB . "}";
				$lines[] = TAB . TAB . "";
				$lines[] = TAB . TAB . "\$query = implode(NL, array(";
				$lines[] = TAB . TAB . TAB . "\"SELECT {\$db->le}{$linktable}{\$db->re}.{\$db->le}{$link["id_column"]}{\$db->re} \",";
				$lines[] = TAB . TAB . TAB . "\"FROM {\$db->le}{$linktable}{\$db->re} \",";
				$lines[] = TAB . TAB . TAB . "\"INNER JOIN {\$db->le}{$linked["relationship"]["table"]}{\$db->re} ON {\$db->le}{$linked["relationship"]["table"]}{\$db->re}.{\$db->le}{$linked["relationship"]["linked"]}{\$db->re}={\$db->le}{$linktable}{\$db->re}.{\$db->le}{$link["id_column"]}{\$db->re} \",";
				$lines[] = TAB . TAB . TAB . "\"	AND {\$db->le}{$linked["relationship"]["table"]}{\$db->re}.{\$db->le}deleted{\$db->re}=0 \",";
				$lines[] = TAB . TAB . TAB . "\"	AND {\$db->le}{$linktable}{\$db->re}.{\$db->le}deleted{\$db->re}=0 \",";
				$lines[] = TAB . TAB . TAB . "\"	AND {\$db->le}{$linked["relationship"]["table"]}{\$db->re}.{\$db->le}{$linked["relationship"]["local"]}{\$db->re}={\$db->queryValue(\$this->id)} \",";
				$lines[] = TAB . TAB . TAB . "(isset(\$type) ? \"	AND {\$db->le}{$linked["relationship"]["table"]}{\$db->re}.{\$db->le}{$linked["relationship"]["table"]}_type_id{\$db->re}=\" . \$db->queryValue({$rel["class"]}::typeId(\$type)) . \" \" : \"\"),";
				$lines[] = TAB . TAB . TAB . "(\$primary ? \"	AND {\$db->le}{$linked["relationship"]["table"]}{\$db->re}.{\$db->le}primary{\$db->re}=1 \" : \"\"),";
				$lines[] = TAB . TAB . TAB . "(isset(\$where) ? \" WHERE {\$where} \" : \"\"),";
				$lines[] = TAB . TAB . TAB . "(isset(\$order) ? \" ORDER BY " . ($link["is_ordered"] ? "\" . alt(\"{\$order}\", \"{\$db->le}{$linktable}{\$db->re}.{\$db->le}order{\$db->re}\")" : "{\$order}\"") . " : \"\"),";
				$lines[] = TAB . TAB . TAB . "(isset(\$limit) ? \" LIMIT {\$offset},{\$limit} \" : \"\")";
				$lines[] = TAB . TAB . "));";
				$lines[] = TAB . TAB . "";
				$lines[] = TAB . TAB . "\$db->query(\$query);";
				$lines[] = TAB . TAB . "";
				$lines[] = TAB . TAB . "while (\$db->getRecord()) {";
				$lines[] = TAB . TAB . "	\$ids[] = \$db->record[\"{$link["id_column"]}\"];";
				$lines[] = TAB . TAB . "}";
				$lines[] = TAB . TAB . "";
				$lines[] = TAB . TAB . "return new Collection(\$ids);";
				$lines[] = TAB . "}";
				$code = implode(NL, $lines);
				if (isset($table["module"]) && isset(self::$tables[$linktable]["site"])) {
					$siteClassCode .= (isset($siteClassCode) ? NL : "") . $code;
				} else {
					$classCode .= (isset($classCode) ? NL : "") . $code;
				}
			}
			
			// THIS IS THE SECOND HALF OF THE SITE RELATIONSHIPS CLASS
			// THE FIRST HALF IS GENERATED IN THE referencedByObjects() METHOD
			$target = dirname(realpath(".")) . "/" . self::$site . "/library/Generated/{$table["class"]}Relationships.php";
			if (isset($siteClassCode) || file_exists($target)) {
				if (file_exists($target)) {
					$existingText = file_get_contents($target);
				} else {
					$existingText = implode(NL, array(
					"<?php",
					"/**",
					" * DO NOT EDIT -- This is an auto-generated class from the Helix Class Generator",
					" * ",
					" * " . self::inheritanceBreadCrumbs($table),
					" */",
					"class {$table["class"]}Relationships extends {$table["class"]}Table {"
					));
				}
				$text = implode(NL, array(
				$existingText,
				$siteClassCode,
				"",
				"}",
				"?>"
				));
				file_put_contents($target, $text);
				self::$linesOfCode += count(explode(NL, $text));
			}
		}
		
		return $classCode;
	}
	
	private static function selfLinkedObjects($table) {
		$lines = array();
		
		$lines[] = "";
		$lines[] = TAB . "public function getParent(\$type=\"default\") {";
		$lines[] = TAB . TAB . "\$db = Database::getInstance();";
		$lines[] = TAB . TAB . "\$relationships = {$table["class"]}{$table["class"]}::objects(null, \"{\$db->le}child_{$table["name"]}_id{\$db->re}='{\$this->id}' AND {\$db->le}{$table["name"]}_{$table["name"]}_type_id{\$db->re}='\" . {$table["class"]}{$table["class"]}::typeId(\$type) . \"'\");";
		$lines[] = TAB . TAB . "return (\$relationships->count()===1) ? \$relationships->get(0)->getParent() : new {$table["class"]}();";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getChildIds() {";
		$lines[] = TAB . TAB . "\$db = Database::getInstance();";
		$lines[] = TAB . TAB . "return {$table["class"]}{$table["class"]}::select(\"{$table["name"]}_{$table["name"]}.child_{$table["name"]}_id\", " . (self::$tables["{$table["name"]}_{$table["name"]}"]["is_ordered"] ? "\"{\$db->le}order{\$db->re} ASC\"" : "null") . ", \"{$table["name"]}_{$table["name"]}.parent_{$table["name"]}_id={\$this->id}\");";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getChildren() {";
		$lines[] = TAB . TAB . "return \$this->hasChildren() ? {$table["class"]}::objects(null, \"{$table["name"]}.{$table["id_column"]} IN (\" . \$this->getChildIds()->join(\",\") . \")\") : new Collection();";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getChildCount() {";
		$lines[] = TAB . TAB . "return \$this->getChildIds()->count();";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function hasChildren() {";
		$lines[] = TAB . TAB . "return \$this->getChildCount()>0;";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function isChild() {";
		$lines[] = TAB . TAB . "return \$this->getParent()->id>0;";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function isRoot() {";
		$lines[] = TAB . TAB . "return \$this->id>0 && !\$this->isChild();";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getSiblingIds() {";
		$lines[] = TAB . TAB . "\$db = Database::getInstance();";
		$lines[] = TAB . TAB . "return {$table["class"]}{$table["class"]}::select(\"{$table["name"]}_{$table["name"]}.child_{$table["name"]}_id\", " . (self::$tables["{$table["name"]}_{$table["name"]}"]["is_ordered"] ? "\"{\$db->le}order{\$db->re} ASC\"" : "null") . ", \"{$table["name"]}_{$table["name"]}.child_{$table["name"]}_id<>'{\$this->id}' AND {$table["name"]}_{$table["name"]}.parent_{$table["name"]}_id=\" . \$db->queryValue(\$this->getParent()->id));";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getSiblings() {";
		$lines[] = TAB . TAB . "return \$this->hasSiblings() ? {$table["class"]}::objects(null, \"{$table["name"]}.{$table["id_column"]} IN (\" . \$this->getSiblingIds()->join(\",\") . \")\") : new Collection();";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getSiblingCount() {";
		$lines[] = TAB . TAB . "return \$this->getSiblingIds()->count();";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function hasSiblings() {";
		$lines[] = TAB . TAB . "return \$this->getSiblingCount()>0;";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getDescendants() {";
		$lines[] = TAB . TAB . "\$descendants = new Collection();";
		$lines[] = TAB . TAB . "foreach (\$this->getChildren() as \$child) {";
		$lines[] = TAB . TAB . "	\$descendants->add(\$child);";
		$lines[] = TAB . TAB . "	if (\$child->hasChildren()) {";
		$lines[] = TAB . TAB . "		\$descendants->add(\$child->getDescendants());";
		$lines[] = TAB . TAB . "	}";
		$lines[] = TAB . TAB . "}";
		$lines[] = TAB . TAB . "return \$descendants;";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getAncestors() {";
		$lines[] = TAB . TAB . "\$ancestors = new Collection();";
		$lines[] = TAB . TAB . "\$child = \$this;";
		$lines[] = TAB . TAB . "while (\$child->isChild()) {";
		$lines[] = TAB . TAB . "	\$parent = \$child->getParent();";
		$lines[] = TAB . TAB . "	\$ancestors->add(\$parent);";
		$lines[] = TAB . TAB . "	\$child = \$parent;";
		$lines[] = TAB . TAB . "}";
		$lines[] = TAB . TAB . "return \$ancestors;";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		$lines[] = TAB . "public function getLevel() {";
		$lines[] = TAB . TAB . "\$level = 0;";
		$lines[] = TAB . TAB . "\$child = \$this;";
		$lines[] = TAB . TAB . "while (\$child->isChild()) {";
		$lines[] = TAB . TAB . "	\$level++;";
		$lines[] = TAB . TAB . "	\$child = \$child->getParent();";
		$lines[] = TAB . TAB . "}";
		$lines[] = TAB . TAB . "return \$level;";
		$lines[] = TAB . "}";
		
		$lines[] = "";
		
		return implode(NL, $lines);
	}
	
	private static function referencedByObjects($table) {
		$classCode = null;
		$siteClassCode = null;
		
		if (isset($table["referenced_by"]) && $table["type"]==="STANDARD") {
			foreach ($table["referenced_by"] as $refbytable=>$referenced_by) {
				$lines = array();
				if (isset($referenced_by["linked_table"])) {
					$refby = self::$tables[$refbytable];
					if ($refby["is_self_referential"]) {continue;}
					$refbyParam = "\$" . lcfirst(self::$tables[$referenced_by["linked_table"]]["class"]) . "Id";
					$columns = array($table["name"]=>"\$this->id", $referenced_by["linked_table"]=>$refbyParam);
					ksort($columns);
					$lines[] = "";
					$lines[] = TAB . "public function get{$refby["class"]}({$refbyParam}, \$type=\"default\") {";
					$lines[] = TAB . TAB . "return new {$refby["class"]}(null, " . implode(", ", $columns) . ", {$refby["class"]}::typeId(\$type));";
					$lines[] = TAB . "}";
					$code = implode(NL, $lines);
					if (isset($table["module"]) && isset(self::$tables[$referenced_by["linked_table"]]["site"])) {
						$siteClassCode .= (isset($siteClassCode) ? NL : "") . $code;
					} else {
						$classCode .= (isset($classCode) ? NL : "") . $code;
					}
				} 
			}
			
			// THIS IS THE FIRST HALF OF THE SITE RELATIONSHIPS CLASS
			// THE SECOND HALF IS GENERATED IN THE linkedObjects() METHOD
			if (isset($siteClassCode)) {
				$text = implode(NL, array(
				"<?php",
				"/**",
				" * DO NOT EDIT -- This is an auto-generated class from the Helix Class Generator",
				" * ",
				" * " . self::inheritanceBreadCrumbs($table),
				" */",
				"class {$table["class"]}Relationships extends {$table["class"]}Table {",
				$siteClassCode
				));
				file_put_contents(dirname(realpath(".")) . "/" . self::$site . "/library/Generated/{$table["class"]}Relationships.php", $text);
				self::$linesOfCode += count(explode(NL, $text));
			}
		}
		
		return $classCode;
	}
	
	private static function referencedObjects($table) {
		$lines = array();
		
		if (isset($table["references"])) {
			foreach ($table["references"] as $reftable=>$reference) {
				$methodClass = preg_match('/^child|parent$/', $reftable) ? ucfirst($reftable) : self::$tables[$reftable]["class"];
				$class = preg_match('/^child|parent$/', $reftable) ? ucfirst($table["parts"][0]) : $methodClass;
				if ($table["type"]!=="RELATIONSHIP" && in_array($reference["local"], $table["primary_keys"])) {
					$prop = "id";
					$readOnly = true;
				} else {
					$prop = $table["columns"][$reference["local"]]["property"];
					$readOnly = false;
				}
				$lines[] = "";
				$lines[] = TAB . "public function get{$methodClass}() {";
				$lines[] = TAB . TAB . "return new {$class}(\$this->{$prop});";
				$lines[] = TAB . "}";
				if ($readOnly) {continue;}
				$param = "\$" . lcfirst($methodClass);
				$lines[] = "";
				$lines[] = TAB . "public function set{$methodClass}({$class} {$param}) {";
				$lines[] = TAB . TAB . "if ({$param}->id>0) {";
				$lines[] = TAB . TAB . TAB . "\$this->{$prop} = {$param}->id;";
				$lines[] = TAB . TAB . "}";
				$lines[] = TAB . "}"; 
			}
		}
		
		return implode(NL, $lines);
	}
	
	private static function typeMethods($table) {
		$lines = array();
		
		if ($table["has_type"]) {
			$lines[] = "";
			$lines[] = TAB . "public static function typeId(\$typeName=null) {";
			$lines[] = TAB . TAB . "\$type = new {$table["class"]}Type(null, \$typeName);";
			$lines[] = TAB . TAB . "return alt(\$type->id, 0);";
			$lines[] = TAB . "}";
			$lines[] = "";
			$lines[] = TAB . "public function getType() {";
			$lines[] = TAB . TAB . "\$type = new {$table["class"]}Type(\$this->{$table["name"]}_type_id);";
			$lines[] = TAB . TAB . "return \$type->name;";
			$lines[] = TAB . "}";
			$lines[] = "";
			$lines[] = TAB . "public function setType(\$typeName=null) {";
			$lines[] = TAB . TAB . "if (isset(\$this->_snapshot)) {return false;}";
			$lines[] = TAB . TAB . "\$type = new {$table["class"]}Type(null, \$typeName);";
			$lines[] = TAB . TAB . "\$this->{$table["name"]}_type_id = \$type->id;";
			$lines[] = TAB . TAB . "return \$this->{$table["name"]}_type_id;";
			$lines[] = TAB . "}";
		}
		
		return implode(NL, $lines);
	}
	
	private static function searchClause($table) {
		$clauses = array();
		foreach ($table["collapsed_columns"] as $colname=>$column) {
			$type = is_array($column["type"]) ? $column["type"][0] : $column["type"];
			if (preg_match('/^char|varchar|tinytext|text|mediumtext|longtext|enum|set$/i', $type)) {
				$clauses[] = "{$column["table"]}.{$colname} LIKE '%{\$keyword}%'";
			}
		}
		return count($clauses)>0 ? implode(" OR ", $clauses) : null;
	}
	
	private static function inheritanceBreadCrumbs($table) {
		$chain = array($table["class"]);
		
		$child = $table;
		while ($child["is_child"]) {
			$parent = self::$tables[$child["parent"]];
			array_unshift($chain, $parent["class"]);
			$child = $parent;
		}
		return "Object -> " . implode(" -> ", $chain);
	}
	
	private static function setProperties($table, $objectVar="this", $valueVar="db->record") {
		$lines = array();
		
		foreach ($table["nice_collapsed_columns"] as $colname=>$column) {
			$type = is_array($column["type"]) ? $column["type"][0] : $column["type"];
			if (preg_match('/^date|time|timestamp|datetime|year$/i',$type)) {
				$val = "new Date(\${$valueVar}[\"{$colname}\"])";
			} else {
				$val = "\${$valueVar}[\"{$colname}\"]";
			}
			$lines[] = TAB . TAB . TAB . TAB . "\${$objectVar}->{$column["property"]} = {$val};";
		}
		
		return implode(NL, $lines);
	}
	
	private static function setInitialValueArray($table, $objectVar="this") {
		$lines = array();
		
		foreach ($table["nice_collapsed_columns"] as $colname=>$column) {
			if ($colname==="mdate" || $colname==="cdate" || $colname==="updated_by_id") {continue;}
			$lines[] = TAB . TAB . "\${$objectVar}->_initial[\"{$column["property"]}\"] = \${$objectVar}->{$column["property"]};";
		}
		
		return implode(NL, $lines);
	}
	
	private static function checkIsDirty($table) {
		$lines = array();
		
		foreach ($table["nice_collapsed_columns"] as $colname=>$column) {
			if ($colname==="mdate" || $colname==="cdate" || $colname==="updated_by_id") {continue;}
			$type = is_array($column["type"]) ? $column["type"][0] : $column["type"];
			if (preg_match('/^smallint|mediumint|int|integer|bigint$/i',$type)>0) {
				$cast = "int";
			} else if (preg_match('/^bit|tinyint$/i',$type)>0) {
				$cast = "bool";
			} else if (preg_match('/^real|double|float|decimal|numeric$/i',$type)>0) {
				$cast = "float";
			} else if (preg_match('/^char|varchar|binary|varbinary|tinyblob|blob|mediumblob|longblob|tinytext|text|mediumtext|longtext|enum|set$/i',$type)>0) {
				$cast = "string";
			} else {
				$cast = "string";
			}
			$comparison = preg_match('/^date|time|timestamp|datetime|year$/i',$type) ? "!=" : "!==";
			$lines[] = TAB . TAB . "\$isDirty = \$isDirty || (({$cast})\$this->{$column["property"]} {$comparison} ({$cast})\$this->_initial[\"{$column["property"]}\"]);";
		}
		
		return implode(NL, $lines);
	}
	
	private static function setInsertStatement($table) {
		$lines = array();
		
		if ($table["is_child"]) {
			$lines[] = TAB . TAB . "if (\$insertParent) {";
			$lines[] = TAB . TAB . "	parent::insert();";
			$lines[] = TAB . TAB . "}";
		} else {
			unset ($table["nice_columns"]["id"]);
			unset ($table["insert_columns"]["id"]);
		}
		
		$lines[] = TAB . TAB . "\$query = implode(NL, array(";
		$lines[] = TAB . TAB . TAB . "\"INSERT INTO {\$db->le}{$table["name"]}{\$db->re} (\",";
		$lines[] = TAB . TAB . TAB . "\"	{\$db->le}" . implode("{\$db->re}, {\$db->le}",array_keys($table["insert_columns"])) . "{\$db->re}\",";
		$lines[] = TAB . TAB . TAB . "\") VALUES (\",";
		foreach ($table["nice_columns"] as $colname=>$column) {
			$type = is_array($column["type"]) ? $column["type"][0] : $column["type"];
			if ($colname==="mdate" || $colname==="cdate") {
				$lines[] = TAB . TAB . TAB . TAB . "\$db->queryValue(timestamp()) . \",\",";
			} else if ($colname=="updated_by_id") {
				$lines[] = TAB . TAB . TAB . TAB . "\$db->queryValue((int)\$session->getUserId()) . \",\",";
			} else if (strstr($type, "int") || strstr($type, "bit")) {
				$lines[] = TAB . TAB . TAB . TAB . "\$db->queryValue((int)\$this->{$column["property"]}) . \",\",";
			} else {
				$lines[] = TAB . TAB . TAB . TAB . "\$db->queryValue(\$this->{$column["property"]}) . \",\",";
			}
		}
		$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -7) . ",";
		$lines[] = TAB . TAB . TAB . "\")\"";
		$lines[] = TAB . TAB . "));";
		$lines[] = TAB . TAB . "\$status = \$db->query(\$query);";
		if (!$table["is_child"]) {
			$lines[] = TAB . TAB . "\$this->id = \$db->getInsertedId();";
		}
		
		return implode(NL, $lines);
	}
	
	private static function setLogStatements($table) {
		$lines = array();
		
		$lines[] = TAB . TAB . "\$query = implode(NL, array(";
		$lines[] = TAB . TAB . TAB . "\"INSERT INTO {\$db->le}{\$log}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re} (\",";
		$lines[] = TAB . TAB . TAB . "\"SELECT\",";
		$lines[] = TAB . TAB . TAB . "\"	NULL,\",";
		$lines[] = TAB . TAB . TAB . "\"	{\$db->le}{\$database}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}mdate{\$db->re},\",";
		$lines[] = TAB . TAB . TAB . "\"	NULL,\",";
		foreach ($table["columns"] as $column) {
			$lines[] = TAB . TAB . TAB . "\"	{\$db->le}{\$database}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}{$column["name"]}{\$db->re},\",";
		}
		$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -3) . "\",";
		$lines[] = TAB . TAB . TAB . "\"FROM {\$db->le}{\$database}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}\",";
		$lines[] = TAB . TAB . TAB . "\"WHERE {\$db->le}{\$database}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}{$table["id_column"]}{\$db->re}={\$db->queryValue(\$this->id)}\",";
		$lines[] = TAB . TAB . TAB . "\")\"";
		$lines[] = TAB . TAB . "));";
		$lines[] = TAB . TAB . "\$status = \$db->query(\$query);";
		$lines[] = "";
		$lines[] = TAB . TAB . "\$logSequence = \$db->getInsertedId();";
		$lines[] = "";
		$lines[] = TAB . TAB . "\$query = implode(NL, array(";
		$lines[] = TAB . TAB . TAB . "\"SELECT {\$db->le}log_sequence{\$db->re}\",";
		$lines[] = TAB . TAB . TAB . "\"FROM {\$db->le}{$table["name"]}{\$db->re}\",";
		$lines[] = TAB . TAB . TAB . "\"WHERE {\$db->le}{$table["name"]}{\$db->re}.{\$db->le}{$table["id_column"]}{\$db->re}={\$db->queryValue(\$this->id)}\",";
		$lines[] = TAB . TAB . TAB . "\"	AND {\$db->le}log_sequence{\$db->re}<'{\$logSequence}'\",";
		$lines[] = TAB . TAB . TAB . "\"ORDER BY {\$db->le}log_sequence{\$db->re} DESC\",";
		$lines[] = TAB . TAB . TAB . "\"LIMIT 0,1\"";
		$lines[] = TAB . TAB . "));";
		$lines[] = TAB . TAB . "\$db->query(\$query);";
		$lines[] = "";
		$lines[] = TAB . TAB . "if (\$db->getRecord()) {";
		$lines[] = TAB . TAB . "	\$updateSequence = (int)\$db->record[\"log_sequence\"];";
		$lines[] = TAB . TAB . "	\$query = implode(NL, array(";
		$lines[] = TAB . TAB . TAB . "	\"UPDATE {\$db->le}{\$log}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}\",";
		$lines[] = TAB . TAB . TAB . "	\"INNER JOIN {\$db->le}{\$database}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re} ON {\$db->le}{\$log}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}{$table["id_column"]}{\$db->re}={\$db->le}{\$database}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}{$table["id_column"]}{\$db->re}\",";
		$lines[] = TAB . TAB . TAB . "	\"	AND {\$db->le}{\$log}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}log_sequence{\$db->re}={\$db->queryValue(\$updateSequence)}\",";
		$lines[] = TAB . TAB . TAB . "	\"SET {\$db->le}{\$log}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}tdate{\$db->re}={\$db->le}{\$database}{\$db->re}.{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}mdate{\$db->re}\"";
		$lines[] = TAB . TAB . "	));";
		$lines[] = TAB . TAB . "	\$db->query(\$query);";
		$lines[] = TAB . TAB . "}";
		
		return implode(NL, $lines);
	}
	
	private static function setUpdateStatement($table) {
		$lines = array();
		
		$idColumn = $table["type"]==="RELATIONSHIP" ? "id" : $table["primary_key"]; 
		
		if (!$table["is_child"]) {
			unset ($table["nice_columns"]["id"]);
		}
		$lines[] = TAB . TAB . TAB . "\$query = implode(NL, array(";
		$lines[] = TAB . TAB . TAB . TAB . "\"UPDATE {\$db->le}{$table["name"]}{\$db->re} SET\",";
		foreach ($table["nice_columns"] as $colname=>$column) {
			if ($colname==="cdate" || ($table["is_child"] && $column["is_primary"])) {continue;}
			$colname = $colname==="id" ? $idColumn : $colname;
			$colprop = $colname===$table["primary_key"] ? "id" : $column["property"];
			$type = is_array($column["type"]) ? $column["type"][0] : $column["type"];
			if ($colname==="mdate" || $colname==="cdate") {
				$lines[] = TAB . TAB . TAB . TAB . TAB . "\"{\$db->le}{$colname}{\$db->re}={\$db->queryValue(timestamp())},\",";
			} else if ($colname==="updated_by_id") {
				$lines[] = TAB . TAB . TAB . TAB . TAB . "\"{\$db->le}{$colname}{\$db->re}={\$db->queryValue((int)\$session->getUserId())},\",";
			} else if (strstr($type, "int")) {
				$lines[] = TAB . TAB . TAB . TAB . TAB . "\"{\$db->le}{$colname}{\$db->re}={\$db->queryValue((int)\$this->{$colprop})},\",";
			} else {
				$lines[] = TAB . TAB . TAB . TAB . TAB . "\"{\$db->le}{$colname}{\$db->re}={\$db->queryValue(\$this->{$colprop})},\",";
			}
		}
		$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -3) . "\",";
		$lines[] = TAB . TAB . TAB . TAB . "\"WHERE {$idColumn}={\$db->queryValue((int)\$this->id)}\"";
		$lines[] = TAB . TAB . TAB . "));";
		
		return implode(NL, $lines);
	}
	
	private static function setSelectStatement($table, $joinLookupTables=false, $fromLogDatabase=false) {
		$lines = array();
		
		$lines[] = TAB . TAB . TAB . "\$query = implode(NL, array(";
		$lines[] = TAB . TAB . TAB . TAB . "\"SELECT " . self::listConstructorQueryColumns($table, $fromLogDatabase) . "\",";			
		$lines[] = TAB . TAB . TAB . TAB . "\"FROM {\$db->le}{$table["name"]}{\$db->re}\"";
		
		if ($table["is_child"]) {
			$lines[count($lines)-1] .= ",";
			$lines[] = self::joinParentTables($table);
		}
		
		if ($joinLookupTables) {
			$lines[] = self::joinLookupTables($table);
		}
		
		if ($table["type"]==="RELATIONSHIP" || (!$joinLookupTables && !$table["is_child"])) {
			$lines[count($lines)-1] .= ",";
			$lines[] = TAB . TAB . TAB . TAB . "\"WHERE {\$condition}\""; 
		}
		
		$lines[] = TAB . TAB . TAB . "));";
		
		return implode(NL, $lines);
	}
	
	private static function setGetStatement($table) {
		$lines = array();
		
		$lines[] = TAB . TAB . "\$query = implode(NL, array(";
		$lines[] = TAB . TAB . TAB . "\"SELECT \" . implode(\",\", \$columns),";			
		$lines[] = TAB . TAB . TAB . "\"FROM {\$db->le}{$table["name"]}{\$db->le}\",";
		if ($table["is_child"]) {
			$lines[] = self::joinParentTables($table, false) . ",";
		}
		$lines[] = self::joinLookupTables($table, $table["is_child"]);
		$lines[] = TAB . TAB . TAB . "\"WHERE {\$db->le}{$table["name"]}{\$db->re}.{\$db->le}deleted{\$db->re}=0\" . (isset(\$where) ? \" AND ({\$where})\" : \"\"),";
		if ($table["is_ordered"]) {
			$lines[] = TAB . TAB . TAB . "\"ORDER BY \" . alt(\$order, \"{\$db->le}order{\$db->re}\"),";
		} else {
			$lines[] = TAB . TAB . TAB . "(isset(\$order) ? \"ORDER BY {\$order}\" : \"\"),";
		}
		$lines[] = TAB . TAB . TAB . "(isset(\$limit) ? \"LIMIT {\$offset},{\$limit}\" : \"\"),";
		$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
		$lines[] = TAB . TAB . "));";
		
		return implode(NL, $lines);
	}
	
	private static function listConstructorQueryColumns($table, $fromLogDatabase=false) {
		$columns = array();
		
		if ($fromLogDatabase) {
			$columns[] = "{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}log_sequence{\$db->re}";
			$columns[] = "{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}fdate{\$db->re}";
			$columns[] = "{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}tdate{\$db->re}";
		}
		
		foreach ($table["nice_collapsed_columns"] as $colname=>$column) {
			$columns[] = "{\$db->le}{$column["table"]}{\$db->re}.{\$db->le}{$colname}{\$db->re}";
		}
		
		return implode(", ", $columns);
	}

	private static function joinLookupTables($table, $joinParentLookups=false) {
		$lines = array();
		
		if (isset($table["references"])) {
			foreach ($table["references"] as $reftable=>$reference) {
				if (in_array($reference["local"], $table["primary_keys"])) {continue;}
				$lines[] = TAB . TAB . TAB . "\"LEFT JOIN {\$db->le}{$reftable}{\$db->re} ON {\$db->le}{$table["name"]}{\$db->re}.{\$db->le}{$reference["local"]}{\$db->re}={\$db->le}{$reftable}{\$db->re}.{\$db->le}{$reference["foreign"]}{\$db->re}\","; 
			}
		}
		
		if ($joinParentLookups) {
			$child = $table;
			while (isset($child)) {
				$parent = self::$tables[$child["parent"]];
				if (isset($parent["references"])) {
					foreach ($parent["references"] as $reftable=>$reference) {
						if (in_array($reference["local"], $parent["primary_keys"])) {continue;}
						$lines[] = TAB . TAB . TAB . "\"LEFT JOIN {\$db->le}{$reftable}{\$db->re} ON {\$db->le}{$parent["name"]}{\$db->re}.{\$db->le}{$reference["local"]}{\$db->re}={\$db->le}{$reftable}{\$db->re}.{\$db->le}{$reference["foreign"]}{\$db->re}\","; 
					}
				}
				$child = $parent["is_child"] ? $parent : null;
			}
		}
		
		return implode(NL, $lines);
	}
	
	private static function setParentJoinCondition($table) {
		$lines = array();
		
		$lines[] = TAB . TAB . "if (isset(\$id)) {"; 
		$lines[] = TAB . TAB . TAB . "\$condition = \"{\$db->le}{$table["name"]}{\$db->re}.{\$db->le}{$table["id_column"]}{\$db->re}={\$db->queryValue(\$id)}\";"; 
		$lines[] = TAB . TAB . "}"; 
		if ($table["type"]==="RELATIONSHIP") {
			$object1 = $table["columns"][$table["primary_keys"][0]]["property"];
			$object2 = $table["columns"][$table["primary_keys"][1]]["property"];
			$relType = $table["columns"][$table["primary_keys"][2]]["property"];
			$lines[count($lines)-1] .= " else if (isset(\${$object1}) && isset(\${$object2})) {";
			$lines[] = TAB . TAB . TAB . "\$condition = \"{\$db->le}{$table["primary_keys"][0]}{\$db->re}={\$db->queryValue(\${$object1})} AND {\$db->le}{$table["primary_keys"][1]}{\$db->re}={\$db->queryValue(\${$object2})} AND {\$db->le}{$table["primary_keys"][2]}{\$db->re}={\$db->queryValue(\${$relType})}\";";
			$lines[] = TAB . TAB . "}";
		} else {
			foreach ($table["collapsed_unique_keys"] as $key) {
				$key = is_array($key) ? $key[0] : $key;
				if ($key==="_id") {continue;}
				$child = $table;
				while (!isset($child["columns"][$key])) {
					$child = self::$tables[$child["parent"]];
				}
				$prop = $child["columns"][$key]["property"];
				$lines[count($lines)-1] .= " else if (isset(\${$prop})) {";
				$lines[] = TAB . TAB . TAB . "\$condition = \"{\$db->le}{$child["name"]}{\$db->re}.{\$db->le}{$prop}{\$db->re}={\$db->queryValue(\${$prop})}\";";
				$lines[] = TAB . TAB . "}";
			}
		}
		
		return implode(NL, $lines);
	}
	
	private static function setSnapshotCondition($table) {
		$lines = array();
		
		$lines[] = TAB . TAB . TAB . "\$condition .= \" AND ({\$db->le}{$table["name"]}{\$db->re}.{\$db->le}tdate{\$db->re} IS NOT NULL AND {\$db->le}{$table["name"]}{\$db->re}.{\$db->le}fdate{\$db->re}<={\$db->queryValue(\$date)} AND {\$db->queryValue(\$date)}<={\$db->le}{$table["name"]}{\$db->re}.{\$db->le}tdate{\$db->re}) \";";
		$child = $table;
		while ($child["is_child"]) {
			$parent = self::$tables[$child["parent"]];
			$lines[] = TAB . TAB . TAB . "\$condition .= \" AND ({\$db->le}{$parent["name"]}{\$db->re}.{\$db->le}tdate{\$db->re} IS NOT NULL AND {\$db->le}{$parent["name"]}{\$db->re}.{\$db->le}fdate{\$db->re}<={\$db->queryValue(\$date)} AND {\$db->queryValue(\$date)}<={\$db->le}{$parent["name"]}{\$db->re}.{\$db->le}tdate{\$db->re}) \";";
			$child = $parent;
		}
		
		return implode(NL, $lines);
	}
	
	private static function joinParentTables($table, $addCondition=true) {
		$lines = array();
		
		$child = $table;
		while (isset($child)) {
			$parent = self::$tables[$child["parent"]];
			$lines[] = TAB . TAB . TAB . "\"INNER JOIN {\$db->le}{$child["parent"]}{\$db->re} ON {\$db->le}{$child["name"]}{\$db->re}.{\$db->le}{$child["primary_key"]}{\$db->re}={\$db->le}{$parent["name"]}{\$db->re}.{\$db->le}{$parent["primary_key"]}{\$db->re}";
			if ($addCondition && !$parent["is_child"]) {
				$lines[count($lines)-1] .= " AND {\$condition}";
			}
			$lines[count($lines)-1] .= "\",";
			$child = $parent["is_child"] ? $parent : null;
		}
		if (count($lines)>0) {
			$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
		}
		
		return implode(NL, $lines);
	}
	
	private static function setPropertyDefaults($table) {
		$lines = array();
		
		foreach ($table["nice_collapsed_columns"] as $colname=>$column) {
			if ($table["is_column_extension"] && $colname==="value") {
				$default = "\$value";
			} else {
				$type = is_array($column["type"]) ? $column["type"][0] : $column["type"];
				$d = $column["default"];
				if (isset($column["references"]) && self::$tables[$column["references"][0]]["is_lookup"]) {
					$d = 1;
				} else if ($type==="tinyint" || $type==="bit") {
					$d = $d===0 ? false : true;
				}
				
				if ($column["is_unique"] || $column["is_primary"]) {
					if ($column["property"]==="hash") {
						$default = "isset(\${$column["property"]}) ? \${$column["property"]} : uniqid()";
					} else {
						$default = "\${$column["property"]}";
					}
				} else {
					$default = json_encode($d);
				}
			}
			$lines[] = TAB . TAB . "\$this->{$column["property"]} = " . (preg_match('/^date|time|timestamp|datetime|year$/i',$type) ? "new Date()" : $default) . ";";
		}
		
		return implode(NL, $lines);
	}
	
	private static function defineProperties($table) {
		$lines = array();
		
		foreach ($table["nice_columns"] as $colname=>$column) {
			if ($colname==="id" && $table["is_child"]) {continue;}
			$lines[] = TAB . "public \${$column["property"]};";
		}
		
		return implode(NL, $lines);
	}
	
	private static function listConstructorArgs($table, $withDefaults=false, $forLogDatabase=false) {
		$text = "";
		
		$defaultText = $withDefaults ? "=null" : "";
		$args = array();
		foreach ($table["primary_keys"] as $primaryKey) {
			$args[] = "\$" . $table["columns"][$primaryKey]["property"] . ($primaryKey==="{$table["name"]}_type_id" && $withDefaults ? "=1" : $defaultText);
		}
		$primaryKeyArgs = implode(", ", $args);
		if ($table["type"]==="RELATIONSHIP") {
			$text .= "\$id{$defaultText}, {$primaryKeyArgs}";
		} else {
			$text .= "\$id{$defaultText}";
			foreach ($table["collapsed_unique_keys"] as $key) {
				if ($key==="_id") {continue;}
				$col = is_array($key) ? $key[0] : $key;
				$child = $table;
				while (!isset($child["columns"][$col])) {
					$child = self::$tables[$child["parent"]];
				}
				$text .= ", \${$child["columns"][$col]["property"]}{$defaultText}";
			}
			
			if ($withDefaults && $table["is_column_extension"] && !$forLogDatabase) {
				$text .= ", \$value=null";
			}
		}
		
		return $text;
	}
	
	public static function writeMySQL($writeLogSchema=false) {
		$lines = array();
		
		$lines[] = "SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;";
		$lines[] = "SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO';";
		$lines[] = "";
		
		foreach (self::$tables as $tablename=>$table) {
			$lines[] = "DROP TABLE IF EXISTS `{$tablename}`;";
			$lines[] = "CREATE TABLE `{$tablename}` (";
			
			if ($writeLogSchema) {
				$lines[] = "  `log_sequence` INT NOT NULL AUTO_INCREMENT,";
				$lines[] = "  `fdate` DATETIME NOT NULL,";
				$lines[] = "  `tdate` DATETIME NULL DEFAULT NULL,";
			}
			
			foreach ($table["columns"] as $colname=>$column) {
				$type = strtoupper(is_array($column["type"]) ? $column["type"][0] . "(" . implode(",",array_slice($column["type"],1)) . ")" : $column["type"]);
				$nullstring = isset($column["nullable"]) ? ($column["nullable"] ? "NULL" : "NOT NULL") : "NULL";
				if (isset($column["references"]) && self::$tables[$column["references"][0]]["is_lookup"]) {
					$def = 1;
				} else {
					$def = $column["default"];
				}
				$defaultstring = isset($def) ? (is_string($def) ? " DEFAULT '{$def}'" : " DEFAULT {$def}") : "";
				$autostring = $column["auto"] && !$writeLogSchema ? " AUTO_INCREMENT" : "";
				$lines[] = "  `{$colname}` {$type} {$nullstring}{$defaultstring}{$autostring},";
			}
			
			if ($writeLogSchema) {
				$lines[] = "  PRIMARY KEY (`log_sequence`)";
			} else {
				$lines[] = "  PRIMARY KEY (`" . implode("`,`",$table["primary_keys"]) . "`)";
				if (isset($table["unique_keys"])) {
					$lines[count($lines)-1] .= ",";
					foreach ($table["unique_keys"] as $key) {
						$lines[] = "  UNIQUE KEY (`" . (is_array($key) ? "{$key[0]}`({$key[1]}))," : "{$key}`),");
					}
					$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
				}
				if (isset($table["references"])) {
					$lines[count($lines)-1] .= ",";
					foreach ($table["references"] as $reftable=>$reference) {
						if ($table["is_self_referential"] && ($reftable==="child" || $reftable==="parent")) {
							$reftable = $table["parts"][0];
						}
						$lines[] = "  FOREIGN KEY (`{$reference["local"]}`) REFERENCES `{$reftable}`(`{$reference["foreign"]}`),";
					}
					$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
				}
			}
			
			$lines[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;";
			$lines[] = "";
		}
		$lines[] = "SET SQL_MODE=@OLD_SQL_MODE;";
		$lines[] = "SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;";
		$text = implode(NL, $lines);
		file_put_contents(Helix::$path . "/temp/helix." . ($writeLogSchema ? "log-" : "") . "schema.mysql.sql",$text);
	}
	
	public static function writeMySQLInserts() {
		$lines = array();
		
		$lines[] = "SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;";
		$lines[] = "SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO';";
		$lines[] = "";
		
		foreach (self::$tables as $tablename=>$table) {
			if (isset($table["data"]) && isset($table["data"]["values"]) && count($table["data"]["values"])>0) {
				$lines[] = "DELETE FROM `{$tablename}`;";
				$lines[] = "ALTER TABLE `{$tablename}` AUTO_INCREMENT=1;";
				$lines[] = "INSERT INTO `{$tablename}` (`" . implode("`,`",$table["data"]["columns"]) . "`) VALUES ";
				foreach ($table["data"]["values"] as $values) {
					foreach ($values as &$value) {
						$value = str_replace("'", "\\'", str_replace("\\", "\\\\", $value));
					}
					$lines[] = "('" . implode("','", $values) . "'),";
				}
				$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1) . ";";
				$lines[] = "";
			}
		}
		$lines[] = "SET SQL_MODE=@OLD_SQL_MODE;";
		$lines[] = "SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;";
		$text = implode(NL, $lines);
		file_put_contents(Helix::$path . "/temp/helix.data.mysql.sql",$text);
	}
	
	public static function writeMSSQL() {
		$lines = array();
		foreach (self::$tables as $tablename=>$table) {
			$lines[] = "IF OBJECT_ID('dbo.[{$tablename}]','U') IS NOT NULL DROP TABLE dbo.[{$tablename}];";
			$lines[] = "CREATE TABLE [{$tablename}] (";
			foreach ($table["columns"] as $colname=>$column) {
				$typeName = self::$types[strtoupper(is_array($column["type"]) ? $column["type"][0] : $column["type"])]["mssql"];
				$type = strtoupper(is_array($column["type"]) ? $typeName . "(" . implode(",",array_slice($column["type"],1)) . ")" : $typeName);
				if (isset($column["key"])) {
					if ((is_array($column["key"]) && $column["key"][0]==="unique") || $column["key"]==="unique") {
						$type = str_replace("VARCHAR(MAX)","VARCHAR(900)",$type);
					}
				}
				$nullstring = isset($column["nullable"]) ? ($column["nullable"] ? "NULL" : "NOT NULL") : "NULL";
				$def = $column["default"];
				$defaultstring = isset($def) ? (is_string($def) ? " DEFAULT '{$def}'" : " DEFAULT {$def}") : "";
				$autostring = $column["auto"] ? " IDENTITY(1,1)" : "";
				$lines[] = "  [{$colname}] {$type} {$nullstring}{$defaultstring}{$autostring},";
			}
			$lines[] = "  PRIMARY KEY ([" . implode("],[",$table["primary_keys"]) . "])";
			if (isset($table["unique_keys"])) {
				$lines[count($lines)-1] .= ",";
				foreach ($table["unique_keys"] as $key) {
					$lines[] = "  UNIQUE ([" . (is_array($key) ? "{$key[0]}])," : "{$key}]),");
				}
				$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
			}
			/*
			if (isset($table["references"])) {
				$lines[count($lines)-1] .= ",";
				foreach ($table["references"] as $reftable=>$reference) {
					$lines[] = "  FOREIGN KEY ([{$reference["local"]}]) REFERENCES [{$reftable}]([{$reference["foreign"]}]),";
				}
				$lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
			}
			*/
			$lines[] = ");";
			$lines[] = "";
		}
		$text = implode(NL, $lines);
		file_put_contents(Helix::$path . "/temp/helix.schema.mssql.sql",$text);
	}
	
	public static function writeSqlite() {
        $lines = array();
        
        foreach (self::$tables as $tablename=>$table) {
            $lines[] = "DROP TABLE IF EXISTS `{$tablename}`;";
            $lines[] = "CREATE TABLE `{$tablename}` (";
            foreach ($table["columns"] as $colname=>$column) {
                $type = strtoupper(is_array($column["type"]) ? $column["type"][0] . "(" . implode(",",array_slice($column["type"],1)) . ")" : $column["type"]);
                $nullstring = isset($column["nullable"]) ? ($column["nullable"] ? "NULL" : "NOT NULL") : "NULL";
                $lines[] = "  `{$colname}` {$type} {$nullstring},";
            }
            $lines[] = "  PRIMARY KEY (`" . implode("`,`",$table["primary_keys"]) . "`)";
            if (isset($table["unique_keys"])) {
                $lines[count($lines)-1] .= ",";
                foreach ($table["unique_keys"] as $key) {
                    $lines[] = "  UNIQUE (`" . (is_array($key) ? "{$key[0]}`)," : "{$key}`),");
                }
                $lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
            }
            if (isset($table["references"])) {
                $lines[count($lines)-1] .= ",";
                foreach ($table["references"] as $reftable=>$reference) {
					if ($table["is_self_referential"] && ($reftable==="child" || $reftable==="parent")) {
						$reftable = $table["parts"][0];
					}
                    $lines[] = "  FOREIGN KEY (`{$reference["local"]}`) REFERENCES `{$reftable}`(`{$reference["foreign"]}`),";
                }
                $lines[count($lines)-1] = substr($lines[count($lines)-1], 0, -1);
            }
            $lines[] = ");";
            $lines[] = "";
        }
        $text = implode(NL, $lines);
        file_put_contents(Helix::$path . "/temp/helix.schema.sqlite.sql",$text);
    }
	
	public static function getType(&$table) {
		$segments = explode("_",$table["name"]);
		if (count($segments)===1) {
			return "STANDARD";
		} else if (count($segments)===2) {
			if (strrchr($table["name"],"_")==="_type") {
				return "TYPE";
			} else if (preg_match('/_(ext|cus)$/i',$table["name"])>0) {
				return "STANDARD";
			} else {
				return "RELATIONSHIP";
			}
		} else if (count($segments)===3 && strrchr($table["name"],"_")==="_type") {
			return "RELATIONSHIP_TYPE";
		} else {
			return "";
		}
	}
	
	public static function getPHPType($type) {
		$type = is_array($type) ? $type[0] : $type;
		if (preg_match('/^smallint|mediumint|int|integer|bigint$/i',$type)>0) {
			return "int";
		} else if (preg_match('/^bit|tinyint$/i',$type)>0) {
			return "bool";
		} else if (preg_match('/^real|double|float|decimal|numeric$/i',$type)>0) {
			return "float";
		} else {
			return "string";
		}
	}
	
}
?>
Return current item: MonoQL